1use crate::auth::UserContext;
16use crate::config::CollectionPolicyConfig;
17use crate::sessions::Session;
18use crate::{Error, Result};
19use serde_json::{Map, Value};
20use std::collections::HashMap;
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum RuleTerm {
25 All,
27 User,
29 Owner,
31 Nobody,
33 Role(String),
35}
36
37impl std::fmt::Display for RuleTerm {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 RuleTerm::All => write!(f, "all"),
41 RuleTerm::User => write!(f, "user"),
42 RuleTerm::Owner => write!(f, "owner"),
43 RuleTerm::Nobody => write!(f, "none"),
44 RuleTerm::Role(r) => write!(f, "{r}"),
45 }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct Rule {
52 terms: Vec<RuleTerm>,
53}
54
55impl Rule {
56 pub fn parse(s: &str, ctx: &str, allow_owner: bool) -> Result<Rule> {
60 let raw: Vec<String> = s
61 .split(',')
62 .map(|t| t.trim().to_string())
63 .filter(|t| !t.is_empty())
64 .collect();
65 if raw.is_empty() {
66 return Err(Error::Config(format!("{ctx}: empty rule")));
67 }
68
69 let mut terms = Vec::new();
70 for word in &raw {
71 let term = match word.as_str() {
72 "all" => RuleTerm::All,
73 "user" => RuleTerm::User,
74 "none" => RuleTerm::Nobody,
75 "owner" => {
76 if !allow_owner {
77 return Err(Error::Config(format!(
78 "{ctx}: \"owner\" is not valid for a create rule (a record has no owner until it is created)"
79 )));
80 }
81 RuleTerm::Owner
82 }
83 other => RuleTerm::Role(other.to_string()),
84 };
85 terms.push(term);
86 }
87
88 if terms.contains(&RuleTerm::All) && terms.len() > 1 {
90 return Err(Error::Config(format!(
91 "{ctx}: \"all\" cannot be combined with other terms"
92 )));
93 }
94 if terms.contains(&RuleTerm::Nobody) && terms.len() > 1 {
95 return Err(Error::Config(format!(
96 "{ctx}: \"none\" cannot be combined with other terms"
97 )));
98 }
99
100 Ok(Rule { terms })
101 }
102
103 pub fn all() -> Rule {
105 Rule { terms: vec![RuleTerm::All] }
106 }
107
108 pub fn owner() -> Rule {
110 Rule { terms: vec![RuleTerm::Owner] }
111 }
112
113 pub fn is_all(&self) -> bool {
115 self.terms == [RuleTerm::All]
116 }
117
118 fn has_owner(&self) -> bool {
120 self.terms.contains(&RuleTerm::Owner)
121 }
122
123 fn non_owner_match(&self, actor: &Actor) -> bool {
126 self.terms.iter().any(|t| match t {
127 RuleTerm::All => true,
128 RuleTerm::User => actor.authenticated,
129 RuleTerm::Role(r) => actor.roles.iter().any(|ar| ar == r),
130 RuleTerm::Owner | RuleTerm::Nobody => false,
131 })
132 }
133}
134
135impl std::fmt::Display for Rule {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 let terms: Vec<String> = self.terms.iter().map(|t| t.to_string()).collect();
139 write!(f, "{}", terms.join(", "))
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
145pub enum OwnerMode {
146 Auto,
148 None,
150}
151
152impl std::fmt::Display for OwnerMode {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 match self {
155 OwnerMode::Auto => write!(f, "auto"),
156 OwnerMode::None => write!(f, "none"),
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq)]
163pub enum Decision {
164 Allow,
166 AllowLegacy,
169 Deny,
171}
172
173#[derive(Debug, Clone, PartialEq)]
175pub enum ReadScope {
176 All,
178 Deny,
180 Filters(Vec<String>),
183}
184
185#[derive(Debug, Clone, Copy, PartialEq)]
187pub enum MutationKind {
188 Update,
189 Delete,
190}
191
192#[derive(Debug, Clone)]
194pub struct CollectionPolicy {
195 pub owner_mode: OwnerMode,
196 pub create: Rule,
197 pub update: Rule,
198 pub delete: Rule,
199 pub read: Rule,
200 pub filter: Option<String>,
201 pub readonly_fields: Vec<String>,
202 pub private_fields: Vec<String>,
203 pub explicit: bool,
205}
206
207impl Default for CollectionPolicy {
208 fn default() -> Self {
210 CollectionPolicy {
211 owner_mode: OwnerMode::Auto,
212 create: Rule::all(),
213 update: Rule::owner(),
214 delete: Rule::owner(),
215 read: Rule::all(),
216 filter: None,
217 readonly_fields: Vec::new(),
218 private_fields: Vec::new(),
219 explicit: false,
220 }
221 }
222}
223
224impl CollectionPolicy {
225 fn compile(name: &str, cfg: &CollectionPolicyConfig) -> Result<Self> {
227 let owner_mode = match cfg.owner.as_deref() {
228 None | Some("auto") => OwnerMode::Auto,
229 Some("none") => OwnerMode::None,
230 Some(other) => {
231 return Err(Error::Config(format!(
232 "collections.{name}.owner: expected \"auto\" or \"none\", got \"{other}\""
233 )));
234 }
235 };
236
237 let create = match &cfg.create {
238 Some(s) => Rule::parse(s, &format!("collections.{name}.create"), false)?,
239 None => Rule::all(),
240 };
241 let update = match &cfg.update {
242 Some(s) => Rule::parse(s, &format!("collections.{name}.update"), true)?,
243 None => Rule::owner(),
244 };
245 let delete = match &cfg.delete {
246 Some(s) => Rule::parse(s, &format!("collections.{name}.delete"), true)?,
247 None => Rule::owner(),
248 };
249 let read = match &cfg.read {
250 Some(s) => Rule::parse(s, &format!("collections.{name}.read"), true)?,
251 None => Rule::all(),
252 };
253
254 Ok(CollectionPolicy {
255 owner_mode,
256 create,
257 update,
258 delete,
259 read,
260 filter: cfg.filter.clone().filter(|f| !f.trim().is_empty()),
261 readonly_fields: cfg.fields.readonly.clone(),
262 private_fields: cfg.fields.private.clone(),
263 explicit: true,
264 })
265 }
266
267 fn mutation_rule(&self, kind: MutationKind) -> &Rule {
268 match kind {
269 MutationKind::Update => &self.update,
270 MutationKind::Delete => &self.delete,
271 }
272 }
273
274 pub fn is_read_scoped(&self) -> bool {
276 !self.read.is_all() || self.filter.is_some()
277 }
278
279 pub fn allows_create(&self, actor: &Actor) -> bool {
281 self.create.non_owner_match(actor)
282 }
283
284 pub fn allows_mutation(
290 &self,
291 actor: &Actor,
292 record: Option<&Value>,
293 kind: MutationKind,
294 resolved_filter: Option<&str>,
295 ) -> Decision {
296 let Some(record) = record else {
298 return Decision::Allow;
299 };
300
301 if let Some(filter) = resolved_filter {
303 if !record_matches_filter(record, filter) {
304 return Decision::Deny;
305 }
306 }
307
308 let rule = self.mutation_rule(kind);
309
310 if rule.non_owner_match(actor) {
312 return Decision::Allow;
313 }
314
315 if rule.has_owner() {
316 let record_owner = record.get("_owner").and_then(|v| v.as_str());
317 match record_owner {
318 Some(owner) if actor.owner_keys().iter().any(|k| k == owner) => {
319 return Decision::Allow;
320 }
321 None => {
322 if !self.explicit {
325 return Decision::AllowLegacy;
326 }
327 }
328 Some(_) => {}
329 }
330 }
331
332 Decision::Deny
333 }
334
335 pub fn read_scope(&self, actor: &Actor, filter_ctx: &HashMap<String, Value>) -> ReadScope {
338 let mut filters: Vec<String> = Vec::new();
339
340 if let Some(raw) = &self.filter {
342 let resolved = crate::parser::replace_variables(raw, filter_ctx);
343 if resolved.contains('#') {
344 return ReadScope::Deny;
345 }
346 filters.push(resolved);
347 }
348
349 if self.read.is_all() {
350 return if filters.is_empty() {
351 ReadScope::All
352 } else {
353 ReadScope::Filters(filters)
354 };
355 }
356
357 if self.read.non_owner_match(actor) {
360 return if filters.is_empty() {
361 ReadScope::All
362 } else {
363 ReadScope::Filters(filters)
364 };
365 }
366
367 if self.read.has_owner() {
369 let keys = actor.owner_keys();
370 if keys.is_empty() {
371 return ReadScope::Deny;
372 }
373 let owner_or = keys
375 .iter()
376 .map(|k| format!("_owner={k}"))
377 .collect::<Vec<_>>()
378 .join(",");
379 filters.push(owner_or);
380 return ReadScope::Filters(filters);
381 }
382
383 ReadScope::Deny
385 }
386
387 pub fn resolved_filter(&self, filter_ctx: &HashMap<String, Value>) -> Option<String> {
389 let raw = self.filter.as_ref()?;
390 let resolved = crate::parser::replace_variables(raw, filter_ctx);
391 if resolved.contains('#') {
392 return Some("_owner=\u{0}__unresolved__".to_string());
395 }
396 Some(resolved)
397 }
398
399 pub fn stamp_owner(&self, map: &mut Map<String, Value>, actor: &Actor) {
401 if self.owner_mode == OwnerMode::None {
402 return;
403 }
404 if let Some(key) = actor.primary_owner_key() {
405 map.insert("_owner".to_string(), Value::String(key));
406 }
407 }
408
409 pub fn sanitize_input(&self, map: &mut Map<String, Value>) {
412 map.remove("_owner");
413 for field in &self.readonly_fields {
414 map.remove(field);
415 }
416 }
417}
418
419#[derive(Debug, Clone)]
421pub struct PolicyRegistry {
422 map: HashMap<String, CollectionPolicy>,
423 default_policy: CollectionPolicy,
424}
425
426impl PolicyRegistry {
427 pub fn from_config(cfg: &HashMap<String, CollectionPolicyConfig>) -> Result<Self> {
429 let mut map = HashMap::new();
430 for (name, entry) in cfg {
431 map.insert(name.clone(), CollectionPolicy::compile(name, entry)?);
432 }
433 Ok(PolicyRegistry {
434 map,
435 default_policy: CollectionPolicy::default(),
436 })
437 }
438
439 pub fn empty() -> Self {
441 PolicyRegistry {
442 map: HashMap::new(),
443 default_policy: CollectionPolicy::default(),
444 }
445 }
446
447 pub fn get(&self, collection: &str) -> &CollectionPolicy {
449 self.map.get(collection).unwrap_or(&self.default_policy)
450 }
451
452 pub fn configured(&self) -> impl Iterator<Item = (&str, &CollectionPolicy)> {
454 self.map.iter().map(|(k, v)| (k.as_str(), v))
455 }
456
457 pub fn is_read_scoped(&self, collection: &str) -> bool {
459 self.get(collection).is_read_scoped()
460 }
461}
462
463#[derive(Debug, Clone)]
465pub struct Actor {
466 pub authenticated: bool,
467 pub user_sub: Option<String>,
468 pub roles: Vec<String>,
469 pub session_key: Option<String>,
471}
472
473impl Actor {
474 pub fn anonymous() -> Actor {
476 Actor {
477 authenticated: false,
478 user_sub: None,
479 roles: Vec::new(),
480 session_key: None,
481 }
482 }
483
484 pub fn from_parts(user: &UserContext, session: Option<&Session>) -> Actor {
486 let user_sub = if user.authenticated {
487 match user.sub() {
488 Some(s) if s.contains([',', '&', '=', '<', '>']) => {
491 tracing::warn!(
492 target: "what::policy",
493 "user sub contains filter metacharacters; skipping user ownership key"
494 );
495 None
496 }
497 other => other,
498 }
499 } else {
500 None
501 };
502 let session_key = session.map(|s| format!("session:{}", &s.id[..s.id.len().min(32)]));
503 Actor {
504 authenticated: user.authenticated,
505 user_sub,
506 roles: if user.authenticated { user.roles() } else { Vec::new() },
507 session_key,
508 }
509 }
510
511 pub fn owner_keys(&self) -> Vec<String> {
513 let mut keys = Vec::new();
514 if let Some(sub) = &self.user_sub {
515 keys.push(format!("user:{sub}"));
516 }
517 if let Some(sk) = &self.session_key {
518 keys.push(sk.clone());
519 }
520 keys
521 }
522
523 pub fn primary_owner_key(&self) -> Option<String> {
526 if let Some(sub) = &self.user_sub {
527 return Some(format!("user:{sub}"));
528 }
529 self.session_key.clone()
530 }
531}
532
533pub fn strip_private_fields(items: &mut Value, private: &[String]) {
535 if private.is_empty() {
536 return;
537 }
538 match items {
539 Value::Array(arr) => {
540 for item in arr.iter_mut() {
541 if let Value::Object(map) = item {
542 for f in private {
543 map.remove(f);
544 }
545 }
546 }
547 }
548 Value::Object(map) => {
549 for f in private {
550 map.remove(f);
551 }
552 }
553 _ => {}
554 }
555}
556
557pub fn scrub_base_context(reg: &PolicyRegistry, ctx: &mut HashMap<String, Value>) {
562 for (name, policy) in reg.configured() {
563 if policy.is_read_scoped() {
564 ctx.remove(name);
565 } else if !policy.private_fields.is_empty() {
566 if let Some(v) = ctx.get_mut(name) {
567 strip_private_fields(v, &policy.private_fields);
568 }
569 }
570 }
571}
572
573pub fn record_matches_filter(record: &Value, filter_expr: &str) -> bool {
577 let obj = match record {
578 Value::Object(m) => m,
579 _ => return false,
580 };
581
582 for group in filter_expr.split(',') {
584 if group.trim().is_empty() {
585 continue;
586 }
587 let mut group_ok = true;
588 for cond in group.split('&') {
589 let cond = cond.trim();
590 if cond.is_empty() {
591 continue;
592 }
593 if !eval_condition(obj, cond) {
594 group_ok = false;
595 break;
596 }
597 }
598 if group_ok {
599 return true;
600 }
601 }
602 false
603}
604
605fn eval_condition(obj: &Map<String, Value>, cond: &str) -> bool {
606 for (op, is_ge, is_le, is_gt, is_lt, is_eq) in [
608 (">=", true, false, false, false, false),
609 ("<=", false, true, false, false, false),
610 (">", false, false, true, false, false),
611 ("<", false, false, false, true, false),
612 ("=", false, false, false, false, true),
613 ] {
614 if let Some((field, val)) = cond.split_once(op) {
615 let field = field.trim();
616 let val = val.trim();
617 let actual = obj.get(field);
618 let actual_str = actual.map(value_to_string).unwrap_or_default();
619
620 let (an, vn) = (actual_str.parse::<f64>(), val.parse::<f64>());
622 return if let (Ok(a), Ok(v)) = (an, vn) {
623 if is_ge {
624 a >= v
625 } else if is_le {
626 a <= v
627 } else if is_gt {
628 a > v
629 } else if is_lt {
630 a < v
631 } else {
632 a == v
633 }
634 } else if is_eq {
635 actual_str == val
636 } else if is_ge {
637 actual_str >= val.to_string()
638 } else if is_le {
639 actual_str <= val.to_string()
640 } else if is_gt {
641 actual_str > val.to_string()
642 } else {
643 actual_str < val.to_string()
644 };
645 }
646 }
647 false
648}
649
650fn value_to_string(v: &Value) -> String {
651 match v {
652 Value::String(s) => s.clone(),
653 Value::Number(n) => n.to_string(),
654 Value::Bool(b) => b.to_string(),
655 Value::Null => String::new(),
656 other => other.to_string(),
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use serde_json::json;
664
665 fn cfg(f: impl FnOnce(&mut CollectionPolicyConfig)) -> CollectionPolicyConfig {
666 let mut c = CollectionPolicyConfig::default();
667 f(&mut c);
668 c
669 }
670
671 fn user_actor(sub: &str, roles: &[&str]) -> Actor {
672 Actor {
673 authenticated: true,
674 user_sub: Some(sub.to_string()),
675 roles: roles.iter().map(|s| s.to_string()).collect(),
676 session_key: None,
677 }
678 }
679
680 fn session_actor(id: &str) -> Actor {
681 Actor {
682 authenticated: false,
683 user_sub: None,
684 roles: Vec::new(),
685 session_key: Some(format!("session:{id}")),
686 }
687 }
688
689 #[test]
690 fn rule_parse_basic() {
691 assert!(Rule::parse("all", "x", true).unwrap().is_all());
692 assert_eq!(
693 Rule::parse("owner, admin", "x", true).unwrap().terms,
694 vec![RuleTerm::Owner, RuleTerm::Role("admin".into())]
695 );
696 assert_eq!(
697 Rule::parse("editor,admin", "x", false).unwrap().terms,
698 vec![RuleTerm::Role("editor".into()), RuleTerm::Role("admin".into())]
699 );
700 }
701
702 #[test]
703 fn rule_parse_failures() {
704 assert!(Rule::parse("owner", "collections.x.create", false).is_err());
705 assert!(Rule::parse("all, admin", "x", true).is_err());
706 assert!(Rule::parse("none, user", "x", true).is_err());
707 assert!(Rule::parse("", "x", true).is_err());
708 }
709
710 #[test]
711 fn default_policy_is_owner_protected() {
712 let p = CollectionPolicy::default();
713 assert!(p.create.is_all());
714 assert_eq!(p.update, Rule::owner());
715 assert_eq!(p.delete, Rule::owner());
716 assert!(p.read.is_all());
717 assert!(!p.explicit);
718 assert!(!p.is_read_scoped());
719 }
720
721 #[test]
722 fn create_authorization() {
723 let p = CollectionPolicy::compile("notes", &cfg(|c| c.create = Some("user".into()))).unwrap();
724 assert!(!p.allows_create(&Actor::anonymous()));
725 assert!(p.allows_create(&user_actor("alice", &[])));
726
727 let roles = CollectionPolicy::compile("a", &cfg(|c| c.create = Some("editor".into()))).unwrap();
728 assert!(!roles.allows_create(&user_actor("bob", &[])));
729 assert!(roles.allows_create(&user_actor("bob", &["editor"])));
730 }
731
732 #[test]
733 fn update_owner_matching() {
734 let p = CollectionPolicy::default(); let rec = json!({"_owner": "user:alice", "title": "x"});
736 assert_eq!(
737 p.allows_mutation(&user_actor("alice", &[]), Some(&rec), MutationKind::Update, None),
738 Decision::Allow
739 );
740 assert_eq!(
741 p.allows_mutation(&user_actor("bob", &[]), Some(&rec), MutationKind::Update, None),
742 Decision::Deny
743 );
744 }
745
746 #[test]
747 fn legacy_unowned_record() {
748 let implicit = CollectionPolicy::default();
749 let rec = json!({"title": "no owner"});
750 assert_eq!(
751 implicit.allows_mutation(&Actor::anonymous(), Some(&rec), MutationKind::Delete, None),
752 Decision::AllowLegacy
753 );
754
755 let explicit = CollectionPolicy::compile("n", &cfg(|c| c.update = Some("owner".into()))).unwrap();
757 assert_eq!(
758 explicit.allows_mutation(&session_actor("abc"), Some(&rec), MutationKind::Update, None),
759 Decision::Deny
760 );
761 }
762
763 #[test]
764 fn missing_record_is_safe_noop() {
765 let p = CollectionPolicy::default();
766 assert_eq!(
767 p.allows_mutation(&Actor::anonymous(), None, MutationKind::Delete, None),
768 Decision::Allow
769 );
770 }
771
772 #[test]
773 fn role_delete_rule() {
774 let p = CollectionPolicy::compile("n", &cfg(|c| c.delete = Some("owner, admin".into()))).unwrap();
775 let rec = json!({"_owner": "user:alice"});
776 assert_eq!(
778 p.allows_mutation(&user_actor("carol", &["admin"]), Some(&rec), MutationKind::Delete, None),
779 Decision::Allow
780 );
781 assert_eq!(
783 p.allows_mutation(&user_actor("carol", &[]), Some(&rec), MutationKind::Delete, None),
784 Decision::Deny
785 );
786 }
787
788 #[test]
789 fn tenant_filter_gates_mutation() {
790 let p = CollectionPolicy::compile("n", &cfg(|c| {
791 c.filter = Some("org=acme".into());
792 c.update = Some("user".into());
793 })).unwrap();
794 let mine = json!({"org": "acme"});
795 let theirs = json!({"org": "other"});
796 assert_eq!(
797 p.allows_mutation(&user_actor("a", &[]), Some(&mine), MutationKind::Update, Some("org=acme")),
798 Decision::Allow
799 );
800 assert_eq!(
801 p.allows_mutation(&user_actor("a", &[]), Some(&theirs), MutationKind::Update, Some("org=acme")),
802 Decision::Deny
803 );
804 }
805
806 #[test]
807 fn read_scope_permutations() {
808 let empty = HashMap::new();
809
810 assert_eq!(CollectionPolicy::default().read_scope(&Actor::anonymous(), &empty), ReadScope::All);
812
813 let owner_read = CollectionPolicy::compile("n", &cfg(|c| c.read = Some("owner".into()))).unwrap();
815 match owner_read.read_scope(&session_actor("abc"), &empty) {
816 ReadScope::Filters(f) => assert_eq!(f, vec!["_owner=session:abc".to_string()]),
817 other => panic!("expected filters, got {other:?}"),
818 }
819
820 assert_eq!(owner_read.read_scope(&Actor::anonymous(), &empty), ReadScope::Deny);
822
823 let user_read = CollectionPolicy::compile("n", &cfg(|c| c.read = Some("user".into()))).unwrap();
825 assert_eq!(user_read.read_scope(&user_actor("a", &[]), &empty), ReadScope::All);
826 assert_eq!(user_read.read_scope(&Actor::anonymous(), &empty), ReadScope::Deny);
827 }
828
829 #[test]
830 fn read_scope_tenant_filter() {
831 let mut ctx = HashMap::new();
832 ctx.insert("user".to_string(), json!({"org_id": "acme"}));
833 let p = CollectionPolicy::compile("n", &cfg(|c| c.filter = Some("org_id=#user.org_id#".into()))).unwrap();
834 match p.read_scope(&user_actor("a", &[]), &ctx) {
835 ReadScope::Filters(f) => assert_eq!(f, vec!["org_id=acme".to_string()]),
836 other => panic!("expected filters, got {other:?}"),
837 }
838 assert_eq!(p.read_scope(&user_actor("a", &[]), &HashMap::new()), ReadScope::Deny);
840 }
841
842 #[test]
843 fn owner_and_tenant_combine() {
844 let mut ctx = HashMap::new();
845 ctx.insert("user".to_string(), json!({"org_id": "acme"}));
846 let p = CollectionPolicy::compile("n", &cfg(|c| {
847 c.read = Some("owner".into());
848 c.filter = Some("org_id=#user.org_id#".into());
849 })).unwrap();
850 let actor = user_actor("alice", &[]);
851 match p.read_scope(&actor, &ctx) {
852 ReadScope::Filters(f) => {
853 assert_eq!(f, vec!["org_id=acme".to_string(), "_owner=user:alice".to_string()]);
854 }
855 other => panic!("expected filters, got {other:?}"),
856 }
857 }
858
859 #[test]
860 fn stamp_and_sanitize() {
861 let p = CollectionPolicy::compile("n", &cfg(|c| c.fields.readonly = vec!["price".into()])).unwrap();
862 let mut map = Map::new();
863 map.insert("title".into(), json!("hi"));
864 map.insert("_owner".into(), json!("user:evil")); map.insert("price".into(), json!("0")); p.sanitize_input(&mut map);
867 assert!(!map.contains_key("_owner"));
868 assert!(!map.contains_key("price"));
869
870 p.stamp_owner(&mut map, &user_actor("alice", &[]));
871 assert_eq!(map.get("_owner"), Some(&json!("user:alice")));
872 }
873
874 #[test]
875 fn owner_mode_none_skips_stamp() {
876 let p = CollectionPolicy::compile("n", &cfg(|c| c.owner = Some("none".into()))).unwrap();
877 let mut map = Map::new();
878 p.stamp_owner(&mut map, &user_actor("alice", &[]));
879 assert!(!map.contains_key("_owner"));
880 }
881
882 #[test]
883 fn actor_owner_keys() {
884 let both = Actor {
885 authenticated: true,
886 user_sub: Some("alice".into()),
887 roles: vec![],
888 session_key: Some("session:abc".into()),
889 };
890 assert_eq!(both.owner_keys(), vec!["user:alice", "session:abc"]);
891 assert_eq!(both.primary_owner_key(), Some("user:alice".into()));
892 assert_eq!(session_actor("abc").primary_owner_key(), Some("session:abc".into()));
893 }
894
895 #[test]
896 fn record_matches_filter_parity() {
897 let rec = json!({"org": "acme", "count": 5});
898 assert!(record_matches_filter(&rec, "org=acme"));
899 assert!(!record_matches_filter(&rec, "org=other"));
900 assert!(record_matches_filter(&rec, "count>=5"));
901 assert!(record_matches_filter(&rec, "count>3"));
902 assert!(!record_matches_filter(&rec, "count>5"));
903 assert!(record_matches_filter(&rec, "org=other,org=acme"));
905 assert!(record_matches_filter(&rec, "org=acme&count=5"));
907 assert!(!record_matches_filter(&rec, "org=acme&count=9"));
908 }
909
910 #[test]
911 fn strip_private() {
912 let mut items = json!([{"email": "a@x.com", "name": "A"}, {"email": "b@x.com", "name": "B"}]);
913 strip_private_fields(&mut items, &["email".to_string()]);
914 assert_eq!(items, json!([{"name": "A"}, {"name": "B"}]));
915 }
916
917 #[test]
918 fn registry_defaults_and_lookup() {
919 let mut cfg_map = HashMap::new();
920 cfg_map.insert("notes".to_string(), cfg(|c| c.read = Some("owner".into())));
921 let reg = PolicyRegistry::from_config(&cfg_map).unwrap();
922 assert!(reg.get("notes").explicit);
923 assert!(reg.is_read_scoped("notes"));
924 assert!(!reg.get("other").explicit);
926 assert!(!reg.is_read_scoped("other"));
927 }
928
929 #[test]
930 fn registry_fails_loud() {
931 let mut cfg_map = HashMap::new();
932 cfg_map.insert("bad".to_string(), cfg(|c| c.create = Some("owner".into())));
933 assert!(PolicyRegistry::from_config(&cfg_map).is_err());
934 }
935
936 #[test]
937 fn scrub_base_context_drops_scoped() {
938 let mut cfg_map = HashMap::new();
939 cfg_map.insert("secret".to_string(), cfg(|c| c.read = Some("owner".into())));
940 cfg_map.insert("public".to_string(), cfg(|c| c.fields.private = vec!["ssn".into()]));
941 let reg = PolicyRegistry::from_config(&cfg_map).unwrap();
942
943 let mut ctx = HashMap::new();
944 ctx.insert("secret".to_string(), json!([{"x": 1}]));
945 ctx.insert("public".to_string(), json!([{"name": "A", "ssn": "123"}]));
946 ctx.insert("open".to_string(), json!([{"y": 2}]));
947
948 scrub_base_context(®, &mut ctx);
949 assert!(!ctx.contains_key("secret"));
950 assert_eq!(ctx.get("public"), Some(&json!([{"name": "A"}])));
951 assert_eq!(ctx.get("open"), Some(&json!([{"y": 2}])));
952 }
953}