1use async_trait::async_trait;
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::Semaphore;
7
8use crate::cache::{AuthzCache, noop_cache};
9use crate::error::AuthzError;
10use crate::model_ast::{AssignableTarget, RelationExpr};
11use crate::policy_provider::PolicyProvider;
12use crate::resolver::{CheckResolver, CheckResult, ResolveCheckRequest};
13use crate::traits::{Tuple, TupleFilter, TupleReader};
14
15fn json_context_to_cel(
17 context: &HashMap<String, serde_json::Value>,
18) -> HashMap<String, crate::cel::Value> {
19 let mut cel_ctx = HashMap::new();
20 for (key, value) in context {
21 if let Some(cel_val) = json_value_to_cel(value) {
22 cel_ctx.insert(key.clone(), cel_val);
23 }
24 }
25 cel_ctx
26}
27
28fn json_value_to_cel(value: &serde_json::Value) -> Option<crate::cel::Value> {
29 match value {
30 serde_json::Value::Bool(b) => Some(crate::cel::Value::Bool(*b)),
31 serde_json::Value::Number(n) => n.as_i64().map(crate::cel::Value::Int),
32 serde_json::Value::String(s) => Some(crate::cel::Value::String(s.clone())),
33 serde_json::Value::Array(arr) => {
34 let items: Vec<crate::cel::Value> = arr.iter().filter_map(json_value_to_cel).collect();
35 Some(crate::cel::Value::List(items))
36 }
37 _ => None,
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum CheckStrategy {
44 #[default]
46 Batch,
47 Parallel,
49}
50
51pub struct CoreResolver<D, P> {
62 datastore: D,
63 policy_provider: P,
64 max_concurrent: usize,
65 read_semaphore: Arc<Semaphore>,
67 result_cache: Arc<dyn AuthzCache<CheckResult>>,
69 tuple_cache: Arc<dyn AuthzCache<Vec<Tuple>>>,
71 strategy: CheckStrategy,
73}
74
75impl<D, P> CoreResolver<D, P>
76where
77 D: TupleReader + Clone + Send + Sync + 'static,
78 P: PolicyProvider + Send + Sync + 'static,
79{
80 pub fn new(datastore: D, policy_provider: P) -> Self {
82 Self {
83 datastore,
84 policy_provider,
85 max_concurrent: 50, read_semaphore: Arc::new(Semaphore::new(50)),
87 result_cache: noop_cache(),
88 tuple_cache: noop_cache(),
89 strategy: CheckStrategy::default(),
90 }
91 }
92
93 pub fn with_strategy(mut self, strategy: CheckStrategy) -> Self {
95 self.strategy = strategy;
96 self
97 }
98
99 pub fn with_max_concurrent(mut self, max: usize) -> Self {
101 self.max_concurrent = max;
102 self.read_semaphore = Arc::new(Semaphore::new(max));
103 self
104 }
105
106 pub fn with_result_cache(mut self, cache: Arc<dyn AuthzCache<CheckResult>>) -> Self {
108 self.result_cache = cache;
109 self
110 }
111
112 pub fn with_tuple_cache(mut self, cache: Arc<dyn AuthzCache<Vec<Tuple>>>) -> Self {
114 self.tuple_cache = cache;
115 self
116 }
117
118 fn result_cache_key(request: &ResolveCheckRequest) -> String {
119 if request.at_revision.is_empty() {
120 format!(
122 "{}:{}:{}:{}:{}",
123 request.object_type,
124 request.object_id,
125 request.relation,
126 request.subject_type,
127 request.subject_id
128 )
129 } else {
130 format!(
131 "{}:{}:{}:{}:{}:{}",
132 request.at_revision,
133 request.object_type,
134 request.object_id,
135 request.relation,
136 request.subject_type,
137 request.subject_id
138 )
139 }
140 }
141
142 fn tuple_cache_key(
143 revision: &str,
144 object_type: &str,
145 object_id: &str,
146 relation: &str,
147 ) -> String {
148 if revision.is_empty() {
149 format!("{}:{}:{}", object_type, object_id, relation)
150 } else {
151 format!("{}:{}:{}:{}", revision, object_type, object_id, relation)
152 }
153 }
154
155 fn resolve_relation_expr<'a>(
157 &'a self,
158 expr: &'a RelationExpr,
159 request: &'a ResolveCheckRequest,
160 ) -> std::pin::Pin<
161 Box<dyn std::future::Future<Output = Result<CheckResult, AuthzError>> + Send + 'a>,
162 > {
163 Box::pin(async move {
164 let expr_type = match expr {
165 RelationExpr::DirectAssignment(_) => "direct",
166 RelationExpr::ComputedUserset(_) => "computed_userset",
167 RelationExpr::TupleToUserset { .. } => "tuple_to_userset",
168 RelationExpr::Union(_) => "union",
169 RelationExpr::Intersection(_) => "intersection",
170 RelationExpr::Exclusion { .. } => "exclusion",
171 };
172 tracing::debug!(expr_type = expr_type, "resolve_expr");
173 match expr {
174 RelationExpr::DirectAssignment(targets) => {
175 self.resolve_direct(targets, request).await
176 }
177 RelationExpr::ComputedUserset(target_relation) => {
178 self.resolve_computed_userset(target_relation, request)
179 .await
180 }
181 RelationExpr::TupleToUserset {
182 tupleset,
183 computed_userset,
184 } => {
185 self.resolve_tuple_to_userset(tupleset, computed_userset, request)
186 .await
187 }
188 RelationExpr::Union(exprs) => self.resolve_union(exprs, request).await,
189 RelationExpr::Intersection(exprs) => {
190 self.resolve_intersection(exprs, request).await
191 }
192 RelationExpr::Exclusion { base, subtract } => {
193 self.resolve_exclusion(base, subtract, request).await
194 }
195 }
196 })
197 }
198
199 async fn resolve_direct(
201 &self,
202 targets: &[AssignableTarget],
203 request: &ResolveCheckRequest,
204 ) -> Result<CheckResult, AuthzError> {
205 let tuples = self
207 .read_tuples_with_contextual(
208 &request.object_type,
209 &request.object_id,
210 &request.relation,
211 request,
212 )
213 .await?;
214
215 tracing::info!(
216 object_type = %request.object_type,
217 object_id = %request.object_id,
218 relation = %request.relation,
219 subject_type = %request.subject_type,
220 subject_id = %request.subject_id,
221 tuples = ?tuples,
222 targets = ?targets,
223 "resolve_direct input"
224 );
225
226 for tuple in &tuples {
228 for target in targets {
229 match target {
230 AssignableTarget::Type(type_name) => {
231 if tuple.subject_type == *type_name
233 && tuple.subject_id == request.subject_id
234 {
235 return Ok(CheckResult::Allowed);
236 }
237 }
238 AssignableTarget::Userset {
239 type_name,
240 relation,
241 } => {
242 let bare_subject_id = tuple
247 .subject_id
248 .split('#')
249 .next()
250 .unwrap_or(&tuple.subject_id)
251 .to_string();
252
253 tracing::info!(
254 object_type = %request.object_type,
255 object_id = %request.object_id,
256 request_relation = %request.relation,
257 tuple_subject_type = %tuple.subject_type,
258 tuple_subject_id = %tuple.subject_id,
259 bare_subject_id = %bare_subject_id,
260 target_type = %type_name,
261 target_relation = %relation,
262 subject_type = %request.subject_type,
263 subject_id = %request.subject_id,
264 "Evaluating userset target"
265 );
266
267 if tuple.subject_type == *type_name {
268 let child_req = request.child_request(
270 type_name.clone(),
271 bare_subject_id,
272 relation.clone(),
273 request.subject_type.clone(),
274 request.subject_id.clone(),
275 );
276 let result = self.resolve_check(child_req).await?;
277 tracing::info!(
278 target_type = %type_name,
279 target_object_id = %tuple.subject_id,
280 target_relation = %relation,
281 result = ?result,
282 "Userset child check result"
283 );
284 if result == CheckResult::Allowed {
285 return Ok(CheckResult::Allowed);
286 }
287 }
288 }
289 AssignableTarget::Wildcard(type_name) => {
290 if tuple.subject_type == *type_name && tuple.subject_id == "*" {
292 if request.subject_type == *type_name {
294 return Ok(CheckResult::Allowed);
295 }
296 }
297 }
298 AssignableTarget::Conditional { target, condition } => {
299 if let AssignableTarget::Type(type_name) = target.as_ref()
302 && tuple.subject_type == *type_name
303 && tuple.subject_id == request.subject_id
304 {
305 if let Some(tuple_condition) = &tuple.condition
307 && tuple_condition == condition
308 {
309 if !request.context.is_empty() {
311 let type_system = self.policy_provider.get_policy().await?;
312 if let Some(cond_def) = type_system.get_condition(condition) {
313 let cel_ctx = json_context_to_cel(&request.context);
314 match crate::cel::compile(&cond_def.expression) {
315 Ok(program) => {
316 match crate::cel::evaluate(&program, &cel_ctx) {
317 Ok(crate::cel::CelResult::Met(true)) => {
318 return Ok(CheckResult::Allowed);
319 }
320 Ok(crate::cel::CelResult::Met(false)) => {
321 }
323 Ok(
324 crate::cel::CelResult::MissingParameters(
325 params,
326 ),
327 ) => {
328 return Ok(CheckResult::ConditionRequired(
329 params,
330 ));
331 }
332 Err(e) => {
333 tracing::warn!(condition = %condition, error = %e, "CEL evaluation error");
334 return Ok(CheckResult::ConditionRequired(
335 vec![condition.clone()],
336 ));
337 }
338 }
339 }
340 Err(e) => {
341 tracing::warn!(condition = %condition, error = %e, "CEL compile error");
342 return Ok(CheckResult::ConditionRequired(vec![
343 condition.clone(),
344 ]));
345 }
346 }
347 } else {
348 return Ok(CheckResult::ConditionRequired(vec![
349 condition.clone(),
350 ]));
351 }
352 } else {
353 return Ok(CheckResult::ConditionRequired(vec![
354 condition.clone(),
355 ]));
356 }
357 }
358 }
359 }
360 }
361 }
362 }
363
364 Ok(CheckResult::Denied)
365 }
366
367 async fn resolve_computed_userset(
369 &self,
370 target_relation: &str,
371 request: &ResolveCheckRequest,
372 ) -> Result<CheckResult, AuthzError> {
373 let child_req = request.child_request(
375 request.object_type.clone(),
376 request.object_id.clone(),
377 target_relation.to_string(),
378 request.subject_type.clone(),
379 request.subject_id.clone(),
380 );
381
382 tracing::info!(
383 object_type = %request.object_type,
384 object_id = %request.object_id,
385 request_relation = %request.relation,
386 target_relation = %target_relation,
387 subject_type = %request.subject_type,
388 subject_id = %request.subject_id,
389 "resolve_computed_userset rewriting to child request"
390 );
391
392 self.resolve_check(child_req).await
393 }
394
395 async fn resolve_tuple_to_userset(
397 &self,
398 tupleset_relation: &str,
399 computed_relation: &str,
400 request: &ResolveCheckRequest,
401 ) -> Result<CheckResult, AuthzError> {
402 let parent_tuples = self
404 .read_tuples_with_contextual(
405 &request.object_type,
406 &request.object_id,
407 tupleset_relation,
408 request,
409 )
410 .await?;
411
412 tracing::info!(
413 object_type = %request.object_type,
414 object_id = %request.object_id,
415 request_relation = %request.relation,
416 tupleset_relation = %tupleset_relation,
417 computed_relation = %computed_relation,
418 parent_tuples = ?parent_tuples,
419 "resolve_tuple_to_userset parent tuples"
420 );
421
422 if parent_tuples.is_empty() {
423 return Ok(CheckResult::Denied);
424 }
425
426 for parent_tuple in parent_tuples {
428 let child_req = request.child_request(
429 parent_tuple.subject_type.clone(),
430 parent_tuple.subject_id.clone(),
431 computed_relation.to_string(),
432 request.subject_type.clone(),
433 request.subject_id.clone(),
434 );
435
436 let result = self.resolve_check(child_req).await?;
437 tracing::info!(
438 parent_type = %parent_tuple.subject_type,
439 parent_id = %parent_tuple.subject_id,
440 computed_relation = %computed_relation,
441 result = ?result,
442 "resolve_tuple_to_userset child result"
443 );
444 if result == CheckResult::Allowed {
445 return Ok(CheckResult::Allowed);
446 }
447 }
448
449 Ok(CheckResult::Denied)
450 }
451
452 async fn resolve_union(
454 &self,
455 exprs: &[RelationExpr],
456 request: &ResolveCheckRequest,
457 ) -> Result<CheckResult, AuthzError> {
458 tracing::info!(
459 object_type = %request.object_type,
460 object_id = %request.object_id,
461 request_relation = %request.relation,
462 subject_type = %request.subject_type,
463 subject_id = %request.subject_id,
464 exprs = ?exprs,
465 strategy = ?self.strategy,
466 "resolve_union evaluating expressions"
467 );
468 match self.strategy {
469 CheckStrategy::Batch => self.resolve_union_batch(exprs, request).await,
470 CheckStrategy::Parallel => self.resolve_union_parallel(exprs, request).await,
471 }
472 }
473
474 async fn resolve_union_batch(
476 &self,
477 exprs: &[RelationExpr],
478 request: &ResolveCheckRequest,
479 ) -> Result<CheckResult, AuthzError> {
480 let mut direct_assignments = Vec::new();
482 let mut other_exprs = Vec::new();
483
484 for expr in exprs {
485 match expr {
486 RelationExpr::DirectAssignment(_) => direct_assignments.push(expr),
487 _ => other_exprs.push(expr),
488 }
489 }
490
491 tracing::info!(
492 direct_assignments_count = direct_assignments.len(),
493 other_exprs_count = other_exprs.len(),
494 "resolve_union_batch categorizing expressions"
495 );
496
497 if !direct_assignments.is_empty() {
498 tracing::info!("resolve_union_batch evaluating direct assignments individually");
500 for expr in &direct_assignments {
501 if let Ok(CheckResult::Allowed) = self.resolve_relation_expr(expr, request).await {
502 return Ok(CheckResult::Allowed);
503 }
504 }
505 }
506
507 tracing::info!("resolve_union_batch falling back to sequential evaluation");
509 for expr in &other_exprs {
510 if let Ok(CheckResult::Allowed) = self.resolve_relation_expr(expr, request).await {
511 return Ok(CheckResult::Allowed);
512 }
513 }
514
515 Ok(CheckResult::Denied)
516 }
517
518 async fn resolve_union_parallel(
520 &self,
521 exprs: &[RelationExpr],
522 request: &ResolveCheckRequest,
523 ) -> Result<CheckResult, AuthzError> {
524 use futures::FutureExt;
525 use futures::future::select_ok;
526
527 if exprs.is_empty() {
528 return Ok(CheckResult::Denied);
529 }
530
531 let mut futures = Vec::new();
533 for expr in exprs {
534 let future = self
535 .resolve_relation_expr(expr, request)
536 .then(|result| async move {
537 match result {
538 Ok(CheckResult::Allowed) => Ok(CheckResult::Allowed),
539 _ => Err(()),
540 }
541 });
542 futures.push(Box::pin(future));
543 }
544
545 match select_ok(futures).await {
547 Ok((result, _)) => Ok(result),
548 Err(_) => Ok(CheckResult::Denied),
549 }
550 }
551
552 async fn resolve_intersection(
554 &self,
555 exprs: &[RelationExpr],
556 request: &ResolveCheckRequest,
557 ) -> Result<CheckResult, AuthzError> {
558 for expr in exprs {
561 let result = self.resolve_relation_expr(expr, request).await?;
562 match result {
563 CheckResult::Denied => return Ok(CheckResult::Denied),
564 CheckResult::ConditionRequired(params) => {
565 return Ok(CheckResult::ConditionRequired(params));
566 }
567 CheckResult::Allowed => continue,
568 }
569 }
570 Ok(CheckResult::Allowed)
571 }
572
573 async fn resolve_exclusion(
581 &self,
582 base: &RelationExpr,
583 subtract: &RelationExpr,
584 request: &ResolveCheckRequest,
585 ) -> Result<CheckResult, AuthzError> {
586 let base_result = self.resolve_relation_expr(base, request).await?;
587 let subtract_result = self.resolve_relation_expr(subtract, request).await?;
588
589 match (base_result, subtract_result) {
592 (CheckResult::Allowed, CheckResult::Denied) => Ok(CheckResult::Allowed),
593 _ => Ok(CheckResult::Denied),
594 }
595 }
596
597 async fn read_tuples_with_contextual(
599 &self,
600 object_type: &str,
601 object_id: &str,
602 relation: &str,
603 request: &ResolveCheckRequest,
604 ) -> Result<Vec<Tuple>, AuthzError> {
605 let cache_key =
606 Self::tuple_cache_key(&request.at_revision, object_type, object_id, relation);
607
608 if request.contextual_tuples.is_empty()
610 && let Some(cached) = self.tuple_cache.get(&cache_key)
611 {
612 tracing::info!(cache_level = "L3", "cache_hit");
613 return Ok(cached);
614 }
615
616 let filter = TupleFilter {
618 object_type: Some(object_type.to_string()),
619 object_id: Some(object_id.to_string()),
620 relation: Some(relation.to_string()),
621 subject_type: None,
622 subject_id: None,
623 };
624
625 let _permit = self.read_semaphore.acquire().await.map_err(|e| {
627 AuthzError::Internal(format!("Failed to acquire read semaphore: {}", e))
628 })?;
629
630 request
632 .metadata
633 .datastore_queries
634 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
635
636 let mut tuples = self.datastore.read_tuples(&filter).await?;
637
638 if request.contextual_tuples.is_empty() {
639 self.tuple_cache.insert(&cache_key, tuples.clone());
640 }
641
642 for ctx_tuple in &request.contextual_tuples {
644 if ctx_tuple.object_type == object_type
645 && ctx_tuple.object_id == object_id
646 && ctx_tuple.relation == relation
647 {
648 if !tuples.iter().any(|t| {
650 t.subject_type == ctx_tuple.subject_type && t.subject_id == ctx_tuple.subject_id
651 }) {
652 tuples.push(ctx_tuple.clone());
653 }
654 }
655 }
656
657 Ok(tuples)
658 }
659}
660
661#[async_trait]
662impl<D, P> CheckResolver for CoreResolver<D, P>
663where
664 D: TupleReader + Clone + Send + Sync + 'static,
665 P: PolicyProvider + Send + Sync + 'static,
666{
667 async fn resolve_check(
668 &self,
669 mut request: ResolveCheckRequest,
670 ) -> Result<CheckResult, AuthzError> {
671 tracing::info!(
672 authz.object_type = %request.object_type,
673 authz.object_id = %request.object_id,
674 authz.relation = %request.relation,
675 authz.depth = request.depth_remaining,
676 authz.dispatch = request.metadata.get_dispatch_count(),
677 "resolve_check",
678 );
679 if request.recursion_config.enable_cycle_detection {
681 let current_key = (
682 request.object_type.clone(),
683 request.object_id.clone(),
684 request.relation.clone(),
685 );
686 if request.visited.contains(¤t_key) {
687 return Ok(CheckResult::Denied); }
689 request.visited.push(current_key);
690 }
691
692 if request.depth_remaining == 0 {
694 return Err(AuthzError::MaxDepthExceeded);
695 }
696
697 let cache_key = Self::result_cache_key(&request);
699 if request.contextual_tuples.is_empty()
700 && let Some(cached) = self.result_cache.get(&cache_key)
701 {
702 tracing::info!(cache_level = "L2", "cache_hit");
703 return Ok(cached);
704 }
705
706 request
708 .metadata
709 .dispatch_count
710 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
711
712 let current_depth = request
714 .recursion_config
715 .max_depth
716 .saturating_sub(request.depth_remaining);
717 request
718 .metadata
719 .max_depth_reached
720 .fetch_max(current_depth, std::sync::atomic::Ordering::Relaxed);
721
722 let type_system = self.policy_provider.get_policy().await?;
724
725 let relation_def = type_system
727 .get_relation(&request.object_type, &request.relation)
728 .ok_or_else(|| {
729 if let Some(type_def) = type_system.get_type(&request.object_type) {
730 tracing::error!(
731 object_type = %request.object_type,
732 relation = %request.relation,
733 available_relations = ?type_def.relations.iter().map(|r| &r.name).collect::<Vec<_>>(),
734 available_permissions = ?type_def.permissions.iter().map(|p| &p.name).collect::<Vec<_>>(),
735 "RelationNotFound error"
736 );
737 }
738 AuthzError::RelationNotFound {
739 object_type: request.object_type.clone(),
740 relation: request.relation.clone(),
741 }
742 })?;
743
744 let relation_expr = relation_def.expression.clone();
746 let result = self.resolve_relation_expr(&relation_expr, &request).await?;
747
748 if request.contextual_tuples.is_empty() {
749 self.result_cache.insert(&cache_key, result.clone());
750 }
751
752 Ok(result)
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759 use crate::model_parser::parse_dsl;
760 use crate::policy_provider::StaticPolicyProvider;
761 use crate::traits::Tuple;
762 use crate::type_system::TypeSystem;
763
764 #[derive(Clone)]
766 struct MockDatastore {
767 tuples: Vec<Tuple>,
768 }
769
770 #[async_trait]
771 impl TupleReader for MockDatastore {
772 async fn read_tuples(&self, filter: &TupleFilter) -> Result<Vec<Tuple>, AuthzError> {
773 Ok(self
774 .tuples
775 .iter()
776 .filter(|t| {
777 filter
778 .object_type
779 .as_ref()
780 .map_or(true, |v| &t.object_type == v)
781 && filter
782 .object_id
783 .as_ref()
784 .map_or(true, |v| &t.object_id == v)
785 && filter.relation.as_ref().map_or(true, |v| &t.relation == v)
786 && filter
787 .subject_type
788 .as_ref()
789 .map_or(true, |v| &t.subject_type == v)
790 && filter
791 .subject_id
792 .as_ref()
793 .map_or(true, |v| &t.subject_id == v)
794 })
795 .cloned()
796 .collect())
797 }
798
799 async fn read_user_tuple(
800 &self,
801 _object_type: &str,
802 _object_id: &str,
803 _relation: &str,
804 _subject_type: &str,
805 _subject_id: &str,
806 ) -> Result<Option<Tuple>, AuthzError> {
807 Ok(None)
808 }
809
810 async fn read_userset_tuples(
811 &self,
812 _object_type: &str,
813 _object_id: &str,
814 _relation: &str,
815 ) -> Result<Vec<Tuple>, AuthzError> {
816 Ok(Vec::new())
817 }
818
819 async fn read_starting_with_user(
820 &self,
821 _subject_type: &str,
822 _subject_id: &str,
823 ) -> Result<Vec<Tuple>, AuthzError> {
824 Ok(Vec::new())
825 }
826
827 async fn read_user_tuple_batch(
828 &self,
829 object_type: &str,
830 object_id: &str,
831 relations: &[String],
832 subject_type: &str,
833 subject_id: &str,
834 ) -> Result<Option<Tuple>, AuthzError> {
835 Ok(self
836 .tuples
837 .iter()
838 .find(|t| {
839 t.object_type == object_type
840 && t.object_id == object_id
841 && relations.iter().any(|r| r == &t.relation)
842 && t.subject_type == subject_type
843 && t.subject_id == subject_id
844 })
845 .cloned())
846 }
847 }
848
849 #[tokio::test]
850 async fn test_direct_user_match() {
851 let dsl = "type document { relations define viewer: [user] }";
852 let model = parse_dsl(dsl).unwrap();
853 let ts = TypeSystem::new(model);
854
855 let tuples = vec![Tuple {
856 object_type: "document".to_string(),
857 object_id: "doc1".to_string(),
858 relation: "viewer".to_string(),
859 subject_type: "user".to_string(),
860 subject_id: "alice".to_string(),
861 condition: None,
862 }];
863
864 let datastore = MockDatastore { tuples };
865 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
866
867 let request = ResolveCheckRequest::new(
868 "document".into(),
869 "doc1".into(),
870 "viewer".into(),
871 "user".into(),
872 "alice".into(),
873 );
874
875 let result = resolver.resolve_check(request).await.unwrap();
876 assert_eq!(result, CheckResult::Allowed);
877 }
878
879 #[tokio::test]
880 async fn test_direct_user_no_match() {
881 let dsl = "type document { relations define viewer: [user] }";
882 let model = parse_dsl(dsl).unwrap();
883 let ts = TypeSystem::new(model);
884
885 let tuples = vec![Tuple {
886 object_type: "document".to_string(),
887 object_id: "doc1".to_string(),
888 relation: "viewer".to_string(),
889 subject_type: "user".to_string(),
890 subject_id: "alice".to_string(),
891 condition: None,
892 }];
893
894 let datastore = MockDatastore { tuples };
895 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
896
897 let request = ResolveCheckRequest::new(
898 "document".into(),
899 "doc1".into(),
900 "viewer".into(),
901 "user".into(),
902 "bob".into(),
903 );
904
905 let result = resolver.resolve_check(request).await.unwrap();
906 assert_eq!(result, CheckResult::Denied);
907 }
908
909 #[tokio::test]
910 async fn test_computed_userset() {
911 let dsl = "type document { relations define viewer: [user] define can_view: viewer }";
912 let model = parse_dsl(dsl).unwrap();
913 let ts = TypeSystem::new(model);
914
915 let tuples = vec![Tuple {
916 object_type: "document".to_string(),
917 object_id: "doc1".to_string(),
918 relation: "viewer".to_string(),
919 subject_type: "user".to_string(),
920 subject_id: "alice".to_string(),
921 condition: None,
922 }];
923
924 let datastore = MockDatastore { tuples };
925 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
926
927 let request = ResolveCheckRequest::new(
928 "document".into(),
929 "doc1".into(),
930 "can_view".into(),
931 "user".into(),
932 "alice".into(),
933 );
934
935 let result = resolver.resolve_check(request).await.unwrap();
936 assert_eq!(result, CheckResult::Allowed);
937 }
938
939 #[tokio::test]
940 async fn test_cycle_detection() {
941 let dsl = r#"
942 type folder {
943 relations
944 define parent: [folder]
945 permissions
946 define view = parent->view
947 }
948 type document {
949 relations
950 define parent: [folder]
951 permissions
952 define view = parent->view
953 }
954 "#;
955 let model = parse_dsl(dsl).unwrap();
956 let ts = TypeSystem::new(model);
957
958 let tuples = vec![
960 Tuple {
961 object_type: "folder".to_string(),
962 object_id: "folder1".to_string(),
963 relation: "parent".to_string(),
964 subject_type: "folder".to_string(),
965 subject_id: "folder2".to_string(),
966 condition: None,
967 },
968 Tuple {
969 object_type: "folder".to_string(),
970 object_id: "folder2".to_string(),
971 relation: "parent".to_string(),
972 subject_type: "folder".to_string(),
973 subject_id: "folder1".to_string(),
974 condition: None,
975 },
976 Tuple {
977 object_type: "document".to_string(),
978 object_id: "doc1".to_string(),
979 relation: "parent".to_string(),
980 subject_type: "folder".to_string(),
981 subject_id: "folder1".to_string(),
982 condition: None,
983 },
984 ];
985
986 let datastore = MockDatastore { tuples };
987 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
988
989 let request = ResolveCheckRequest::new(
990 "document".into(),
991 "doc1".into(),
992 "view".into(),
993 "user".into(),
994 "alice".into(),
995 );
996
997 let result = resolver.resolve_check(request).await.unwrap();
998 assert_eq!(result, CheckResult::Denied); }
1000
1001 #[tokio::test]
1002 async fn test_recursion_config_depth_first() {
1003 let dsl = r#"
1004 type folder {
1005 relations
1006 define parent: [folder]
1007 permissions
1008 define view = parent->view
1009 }
1010 type document {
1011 relations
1012 define parent: [folder]
1013 permissions
1014 define view = parent->view
1015 }
1016 "#;
1017 let model = parse_dsl(dsl).unwrap();
1018 let ts = TypeSystem::new(model);
1019
1020 let tuples = vec![
1022 Tuple {
1023 object_type: "folder".to_string(),
1024 object_id: "folder1".to_string(),
1025 relation: "parent".to_string(),
1026 subject_type: "folder".to_string(),
1027 subject_id: "folder2".to_string(),
1028 condition: None,
1029 },
1030 Tuple {
1031 object_type: "folder".to_string(),
1032 object_id: "folder2".to_string(),
1033 relation: "parent".to_string(),
1034 subject_type: "folder".to_string(),
1035 subject_id: "folder1".to_string(),
1036 condition: None,
1037 },
1038 Tuple {
1039 object_type: "document".to_string(),
1040 object_id: "doc1".to_string(),
1041 relation: "parent".to_string(),
1042 subject_type: "folder".to_string(),
1043 subject_id: "folder1".to_string(),
1044 condition: None,
1045 },
1046 ];
1047
1048 let datastore = MockDatastore { tuples };
1049 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1050
1051 let config = crate::resolver::RecursionConfig::depth_first()
1053 .max_depth(10)
1054 .cycle_detection(true);
1055
1056 let request = crate::resolver::ResolveCheckRequest::with_config(
1057 "document".into(),
1058 "doc1".into(),
1059 "view".into(),
1060 "user".into(),
1061 "alice".into(),
1062 config,
1063 );
1064
1065 let result = resolver.resolve_check(request).await.unwrap();
1066 assert_eq!(result, CheckResult::Denied); }
1068
1069 #[tokio::test]
1070 async fn test_recursion_config_breadth_first() {
1071 let dsl = r#"
1072 type folder {
1073 relations
1074 define owner: [user]
1075 permissions
1076 define view = owner
1077 }
1078 type document {
1079 relations
1080 define parent: [folder]
1081 permissions
1082 define view = parent->view
1083 }
1084 "#;
1085 let model = parse_dsl(dsl).unwrap();
1086 let ts = TypeSystem::new(model);
1087
1088 let tuples = vec![
1089 Tuple {
1090 object_type: "folder".to_string(),
1091 object_id: "folder1".to_string(),
1092 relation: "owner".to_string(),
1093 subject_type: "user".to_string(),
1094 subject_id: "alice".to_string(),
1095 condition: None,
1096 },
1097 Tuple {
1098 object_type: "document".to_string(),
1099 object_id: "doc1".to_string(),
1100 relation: "parent".to_string(),
1101 subject_type: "folder".to_string(),
1102 subject_id: "folder1".to_string(),
1103 condition: None,
1104 },
1105 ];
1106
1107 let datastore = MockDatastore { tuples };
1108 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1109
1110 let config = crate::resolver::RecursionConfig::breadth_first()
1112 .max_depth(50)
1113 .cycle_detection(true);
1114
1115 let request = crate::resolver::ResolveCheckRequest::with_config(
1116 "document".into(),
1117 "doc1".into(),
1118 "view".into(),
1119 "user".into(),
1120 "alice".into(),
1121 config,
1122 );
1123
1124 let result = resolver.resolve_check(request).await.unwrap();
1125 assert_eq!(result, CheckResult::Allowed); }
1127
1128 #[tokio::test]
1129 async fn test_cycle_detection_disabled() {
1130 let dsl = "type document { relations define viewer: viewer }";
1131 let model = parse_dsl(dsl).unwrap();
1132 let ts = TypeSystem::new(model);
1133
1134 let datastore = MockDatastore { tuples: vec![] };
1135 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1136
1137 let config = crate::resolver::RecursionConfig::depth_first().cycle_detection(false);
1139
1140 let mut request = crate::resolver::ResolveCheckRequest::with_config(
1141 "document".into(),
1142 "doc1".into(),
1143 "viewer".into(),
1144 "user".into(),
1145 "alice".into(),
1146 config,
1147 );
1148 request.depth_remaining = 1;
1149
1150 let result = resolver.resolve_check(request).await;
1151 assert!(result.is_err()); assert!(matches!(result.unwrap_err(), AuthzError::MaxDepthExceeded));
1153 }
1154
1155 #[tokio::test]
1156 async fn test_depth_limit() {
1157 let dsl = "type document { relations define viewer: viewer }";
1158 let model = parse_dsl(dsl).unwrap();
1159 let ts = TypeSystem::new(model);
1160
1161 let datastore = MockDatastore { tuples: vec![] };
1162 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1163
1164 let mut request = ResolveCheckRequest::new(
1165 "document".into(),
1166 "doc1".into(),
1167 "viewer".into(),
1168 "user".into(),
1169 "alice".into(),
1170 );
1171 request.depth_remaining = 1;
1172
1173 let result = resolver.resolve_check(request).await.unwrap();
1174 assert_eq!(result, CheckResult::Denied); }
1176
1177 #[tokio::test]
1178 async fn test_union_first_succeeds() {
1179 let dsl = "type document { relations define viewer: [user] define editor: [user] define can_view: viewer + editor }";
1180 let model = parse_dsl(dsl).unwrap();
1181 let ts = TypeSystem::new(model);
1182
1183 let tuples = vec![Tuple {
1184 object_type: "document".to_string(),
1185 object_id: "doc1".to_string(),
1186 relation: "viewer".to_string(),
1187 subject_type: "user".to_string(),
1188 subject_id: "alice".to_string(),
1189 condition: None,
1190 }];
1191
1192 let datastore = MockDatastore { tuples };
1193 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1194
1195 let request = ResolveCheckRequest::new(
1196 "document".into(),
1197 "doc1".into(),
1198 "can_view".into(),
1199 "user".into(),
1200 "alice".into(),
1201 );
1202
1203 let result = resolver.resolve_check(request).await.unwrap();
1204 assert_eq!(result, CheckResult::Allowed);
1205 }
1206
1207 #[tokio::test]
1208 async fn test_contextual_tuples() {
1209 let dsl = "type document { relations define viewer: [user] }";
1210 let model = parse_dsl(dsl).unwrap();
1211 let ts = TypeSystem::new(model);
1212
1213 let datastore = MockDatastore { tuples: vec![] };
1214 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1215
1216 let mut request = ResolveCheckRequest::new(
1217 "document".into(),
1218 "doc1".into(),
1219 "viewer".into(),
1220 "user".into(),
1221 "alice".into(),
1222 );
1223 request.contextual_tuples = vec![Tuple {
1224 object_type: "document".to_string(),
1225 object_id: "doc1".to_string(),
1226 relation: "viewer".to_string(),
1227 subject_type: "user".to_string(),
1228 subject_id: "alice".to_string(),
1229 condition: None,
1230 }];
1231
1232 let result = resolver.resolve_check(request).await.unwrap();
1233 assert_eq!(result, CheckResult::Allowed);
1234 }
1235
1236 #[tokio::test]
1239 async fn test_intersection_both_allowed() {
1240 let dsl = "type document { relations define viewer: [user] define editor: [user] define can_view: viewer & editor }";
1241 let model = parse_dsl(dsl).unwrap();
1242 let ts = TypeSystem::new(model);
1243
1244 let tuples = vec![
1245 Tuple {
1246 object_type: "document".to_string(),
1247 object_id: "doc1".to_string(),
1248 relation: "viewer".to_string(),
1249 subject_type: "user".to_string(),
1250 subject_id: "alice".to_string(),
1251 condition: None,
1252 },
1253 Tuple {
1254 object_type: "document".to_string(),
1255 object_id: "doc1".to_string(),
1256 relation: "editor".to_string(),
1257 subject_type: "user".to_string(),
1258 subject_id: "alice".to_string(),
1259 condition: None,
1260 },
1261 ];
1262
1263 let datastore = MockDatastore { tuples };
1264 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1265
1266 let request = ResolveCheckRequest::new(
1267 "document".into(),
1268 "doc1".into(),
1269 "can_view".into(),
1270 "user".into(),
1271 "alice".into(),
1272 );
1273
1274 let result = resolver.resolve_check(request).await.unwrap();
1275 assert_eq!(result, CheckResult::Allowed);
1276 }
1277
1278 #[tokio::test]
1279 async fn test_intersection_one_denied() {
1280 let dsl = "type document { relations define viewer: [user] define editor: [user] define can_view: viewer & editor }";
1281 let model = parse_dsl(dsl).unwrap();
1282 let ts = TypeSystem::new(model);
1283
1284 let tuples = vec![Tuple {
1285 object_type: "document".to_string(),
1286 object_id: "doc1".to_string(),
1287 relation: "viewer".to_string(),
1288 subject_type: "user".to_string(),
1289 subject_id: "alice".to_string(),
1290 condition: None,
1291 }];
1292
1293 let datastore = MockDatastore { tuples };
1294 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1295
1296 let request = ResolveCheckRequest::new(
1297 "document".into(),
1298 "doc1".into(),
1299 "can_view".into(),
1300 "user".into(),
1301 "alice".into(),
1302 );
1303
1304 let result = resolver.resolve_check(request).await.unwrap();
1305 assert_eq!(result, CheckResult::Denied);
1306 }
1307
1308 #[tokio::test]
1309 async fn test_intersection_short_circuit_on_denied() {
1310 let dsl = "type document { relations define viewer: [user] define editor: [user] define can_view: viewer & editor }";
1311 let model = parse_dsl(dsl).unwrap();
1312 let ts = TypeSystem::new(model);
1313
1314 let datastore = MockDatastore { tuples: vec![] };
1315 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1316
1317 let request = ResolveCheckRequest::new(
1318 "document".into(),
1319 "doc1".into(),
1320 "can_view".into(),
1321 "user".into(),
1322 "alice".into(),
1323 );
1324
1325 let result = resolver.resolve_check(request).await.unwrap();
1326 assert_eq!(result, CheckResult::Denied);
1327 }
1328
1329 #[tokio::test]
1332 async fn test_exclusion_base_allowed_subtract_denied() {
1333 let dsl = "type document { relations define viewer: [user] define banned: [user] define can_view: viewer - banned }";
1334 let model = parse_dsl(dsl).unwrap();
1335 let ts = TypeSystem::new(model);
1336
1337 let tuples = vec![Tuple {
1338 object_type: "document".to_string(),
1339 object_id: "doc1".to_string(),
1340 relation: "viewer".to_string(),
1341 subject_type: "user".to_string(),
1342 subject_id: "alice".to_string(),
1343 condition: None,
1344 }];
1345
1346 let datastore = MockDatastore { tuples };
1347 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1348
1349 let request = ResolveCheckRequest::new(
1350 "document".into(),
1351 "doc1".into(),
1352 "can_view".into(),
1353 "user".into(),
1354 "alice".into(),
1355 );
1356
1357 let result = resolver.resolve_check(request).await.unwrap();
1358 assert_eq!(result, CheckResult::Allowed);
1359 }
1360
1361 #[tokio::test]
1362 async fn test_exclusion_base_allowed_subtract_allowed() {
1363 let dsl = "type document { relations define viewer: [user] define banned: [user] define can_view: viewer - banned }";
1364 let model = parse_dsl(dsl).unwrap();
1365 let ts = TypeSystem::new(model);
1366
1367 let tuples = vec![
1368 Tuple {
1369 object_type: "document".to_string(),
1370 object_id: "doc1".to_string(),
1371 relation: "viewer".to_string(),
1372 subject_type: "user".to_string(),
1373 subject_id: "alice".to_string(),
1374 condition: None,
1375 },
1376 Tuple {
1377 object_type: "document".to_string(),
1378 object_id: "doc1".to_string(),
1379 relation: "banned".to_string(),
1380 subject_type: "user".to_string(),
1381 subject_id: "alice".to_string(),
1382 condition: None,
1383 },
1384 ];
1385
1386 let datastore = MockDatastore { tuples };
1387 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1388
1389 let request = ResolveCheckRequest::new(
1390 "document".into(),
1391 "doc1".into(),
1392 "can_view".into(),
1393 "user".into(),
1394 "alice".into(),
1395 );
1396
1397 let result = resolver.resolve_check(request).await.unwrap();
1398 assert_eq!(result, CheckResult::Denied);
1399 }
1400
1401 #[tokio::test]
1402 async fn test_exclusion_base_denied() {
1403 let dsl = "type document { relations define viewer: [user] define banned: [user] define can_view: viewer - banned }";
1404 let model = parse_dsl(dsl).unwrap();
1405 let ts = TypeSystem::new(model);
1406
1407 let datastore = MockDatastore { tuples: vec![] };
1408 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1409
1410 let request = ResolveCheckRequest::new(
1411 "document".into(),
1412 "doc1".into(),
1413 "can_view".into(),
1414 "user".into(),
1415 "alice".into(),
1416 );
1417
1418 let result = resolver.resolve_check(request).await.unwrap();
1419 assert_eq!(result, CheckResult::Denied);
1420 }
1421
1422 #[tokio::test]
1425 async fn test_ttu_single_parent() {
1426 let dsl = "type folder { relations define viewer: [user] } type document { relations define parent: [folder] define viewer: parent->viewer }";
1427 let model = parse_dsl(dsl).unwrap();
1428 let ts = TypeSystem::new(model);
1429
1430 let tuples = vec![
1431 Tuple {
1432 object_type: "document".to_string(),
1433 object_id: "doc1".to_string(),
1434 relation: "parent".to_string(),
1435 subject_type: "folder".to_string(),
1436 subject_id: "root".to_string(),
1437 condition: None,
1438 },
1439 Tuple {
1440 object_type: "folder".to_string(),
1441 object_id: "root".to_string(),
1442 relation: "viewer".to_string(),
1443 subject_type: "user".to_string(),
1444 subject_id: "alice".to_string(),
1445 condition: None,
1446 },
1447 ];
1448
1449 let datastore = MockDatastore { tuples };
1450 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1451
1452 let request = ResolveCheckRequest::new(
1453 "document".into(),
1454 "doc1".into(),
1455 "viewer".into(),
1456 "user".into(),
1457 "alice".into(),
1458 );
1459
1460 let result = resolver.resolve_check(request).await.unwrap();
1461 assert_eq!(result, CheckResult::Allowed);
1462 }
1463
1464 #[tokio::test]
1465 async fn test_ttu_no_parent() {
1466 let dsl = "type folder { relations define viewer: [user] } type document { relations define parent: [folder] define viewer: parent->viewer }";
1467 let model = parse_dsl(dsl).unwrap();
1468 let ts = TypeSystem::new(model);
1469
1470 let datastore = MockDatastore { tuples: vec![] };
1471 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1472
1473 let request = ResolveCheckRequest::new(
1474 "document".into(),
1475 "doc1".into(),
1476 "viewer".into(),
1477 "user".into(),
1478 "alice".into(),
1479 );
1480
1481 let result = resolver.resolve_check(request).await.unwrap();
1482 assert_eq!(result, CheckResult::Denied);
1483 }
1484
1485 #[tokio::test]
1486 async fn test_ttu_multiple_parents() {
1487 let dsl = "type folder { relations define viewer: [user] } type document { relations define parent: [folder] define viewer: parent->viewer }";
1488 let model = parse_dsl(dsl).unwrap();
1489 let ts = TypeSystem::new(model);
1490
1491 let tuples = vec![
1492 Tuple {
1493 object_type: "document".to_string(),
1494 object_id: "doc1".to_string(),
1495 relation: "parent".to_string(),
1496 subject_type: "folder".to_string(),
1497 subject_id: "folder1".to_string(),
1498 condition: None,
1499 },
1500 Tuple {
1501 object_type: "document".to_string(),
1502 object_id: "doc1".to_string(),
1503 relation: "parent".to_string(),
1504 subject_type: "folder".to_string(),
1505 subject_id: "folder2".to_string(),
1506 condition: None,
1507 },
1508 Tuple {
1509 object_type: "folder".to_string(),
1510 object_id: "folder2".to_string(),
1511 relation: "viewer".to_string(),
1512 subject_type: "user".to_string(),
1513 subject_id: "alice".to_string(),
1514 condition: None,
1515 },
1516 ];
1517
1518 let datastore = MockDatastore { tuples };
1519 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1520
1521 let request = ResolveCheckRequest::new(
1522 "document".into(),
1523 "doc1".into(),
1524 "viewer".into(),
1525 "user".into(),
1526 "alice".into(),
1527 );
1528
1529 let result = resolver.resolve_check(request).await.unwrap();
1530 assert_eq!(result, CheckResult::Allowed);
1531 }
1532
1533 #[tokio::test]
1536 async fn test_wildcard_match() {
1537 let dsl = "type document { relations define viewer: [user:*] }";
1538 let model = parse_dsl(dsl).unwrap();
1539 let ts = TypeSystem::new(model);
1540
1541 let tuples = vec![Tuple {
1542 object_type: "document".to_string(),
1543 object_id: "doc1".to_string(),
1544 relation: "viewer".to_string(),
1545 subject_type: "user".to_string(),
1546 subject_id: "*".to_string(),
1547 condition: None,
1548 }];
1549
1550 let datastore = MockDatastore { tuples };
1551 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1552
1553 let request = ResolveCheckRequest::new(
1554 "document".into(),
1555 "doc1".into(),
1556 "viewer".into(),
1557 "user".into(),
1558 "alice".into(),
1559 );
1560
1561 let result = resolver.resolve_check(request).await.unwrap();
1562 assert_eq!(result, CheckResult::Allowed);
1563 }
1564
1565 #[tokio::test]
1566 async fn test_wildcard_wrong_type() {
1567 let dsl = "type document { relations define viewer: [user:*] }";
1568 let model = parse_dsl(dsl).unwrap();
1569 let ts = TypeSystem::new(model);
1570
1571 let tuples = vec![Tuple {
1572 object_type: "document".to_string(),
1573 object_id: "doc1".to_string(),
1574 relation: "viewer".to_string(),
1575 subject_type: "user".to_string(),
1576 subject_id: "*".to_string(),
1577 condition: None,
1578 }];
1579
1580 let datastore = MockDatastore { tuples };
1581 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1582
1583 let request = ResolveCheckRequest::new(
1584 "document".into(),
1585 "doc1".into(),
1586 "viewer".into(),
1587 "employee".into(),
1588 "alice".into(),
1589 );
1590
1591 let result = resolver.resolve_check(request).await.unwrap();
1592 assert_eq!(result, CheckResult::Denied);
1593 }
1594
1595 #[tokio::test]
1598 async fn test_userset_expansion() {
1599 let dsl = "type group { relations define member: [user] } type document { relations define viewer: [group#member] }";
1600 let model = parse_dsl(dsl).unwrap();
1601 let ts = TypeSystem::new(model);
1602
1603 let tuples = vec![
1604 Tuple {
1605 object_type: "document".to_string(),
1606 object_id: "doc1".to_string(),
1607 relation: "viewer".to_string(),
1608 subject_type: "group".to_string(),
1609 subject_id: "eng".to_string(),
1610 condition: None,
1611 },
1612 Tuple {
1613 object_type: "group".to_string(),
1614 object_id: "eng".to_string(),
1615 relation: "member".to_string(),
1616 subject_type: "user".to_string(),
1617 subject_id: "alice".to_string(),
1618 condition: None,
1619 },
1620 ];
1621
1622 let datastore = MockDatastore { tuples };
1623 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1624
1625 let request = ResolveCheckRequest::new(
1626 "document".into(),
1627 "doc1".into(),
1628 "viewer".into(),
1629 "user".into(),
1630 "alice".into(),
1631 );
1632
1633 let result = resolver.resolve_check(request).await.unwrap();
1634 assert_eq!(result, CheckResult::Allowed);
1635 }
1636
1637 #[tokio::test]
1638 async fn test_userset_expansion_no_member() {
1639 let dsl = "type group { relations define member: [user] } type document { relations define viewer: [group#member] }";
1640 let model = parse_dsl(dsl).unwrap();
1641 let ts = TypeSystem::new(model);
1642
1643 let tuples = vec![Tuple {
1644 object_type: "document".to_string(),
1645 object_id: "doc1".to_string(),
1646 relation: "viewer".to_string(),
1647 subject_type: "group".to_string(),
1648 subject_id: "eng".to_string(),
1649 condition: None,
1650 }];
1651
1652 let datastore = MockDatastore { tuples };
1653 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1654
1655 let request = ResolveCheckRequest::new(
1656 "document".into(),
1657 "doc1".into(),
1658 "viewer".into(),
1659 "user".into(),
1660 "alice".into(),
1661 );
1662
1663 let result = resolver.resolve_check(request).await.unwrap();
1664 assert_eq!(result, CheckResult::Denied);
1665 }
1666
1667 #[tokio::test]
1670 async fn test_conditional_returns_condition_required() {
1671 let dsl = "type document { relations define viewer: [user with ip_check] } condition ip_check(ip: string) { ip == \"127.0.0.1\" }";
1672 let model = parse_dsl(dsl).unwrap();
1673 let ts = TypeSystem::new(model);
1674
1675 let tuples = vec![Tuple {
1676 object_type: "document".to_string(),
1677 object_id: "doc1".to_string(),
1678 relation: "viewer".to_string(),
1679 subject_type: "user".to_string(),
1680 subject_id: "alice".to_string(),
1681 condition: Some("ip_check".to_string()),
1682 }];
1683
1684 let datastore = MockDatastore { tuples };
1685 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1686
1687 let request = ResolveCheckRequest::new(
1688 "document".into(),
1689 "doc1".into(),
1690 "viewer".into(),
1691 "user".into(),
1692 "alice".into(),
1693 );
1694
1695 let result = resolver.resolve_check(request).await.unwrap();
1696 assert_eq!(
1697 result,
1698 CheckResult::ConditionRequired(vec!["ip_check".to_string()])
1699 );
1700 }
1701
1702 #[tokio::test]
1705 async fn test_result_cache_hit() {
1706 let dsl = "type document { relations define viewer: [user] }";
1707 let model = parse_dsl(dsl).unwrap();
1708 let ts = TypeSystem::new(model);
1709
1710 let tuples = vec![Tuple {
1711 object_type: "document".to_string(),
1712 object_id: "doc1".to_string(),
1713 relation: "viewer".to_string(),
1714 subject_type: "user".to_string(),
1715 subject_id: "alice".to_string(),
1716 condition: None,
1717 }];
1718
1719 let datastore = MockDatastore { tuples };
1720 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1721
1722 let request = ResolveCheckRequest::new(
1723 "document".into(),
1724 "doc1".into(),
1725 "viewer".into(),
1726 "user".into(),
1727 "alice".into(),
1728 );
1729
1730 let result1 = resolver.resolve_check(request.clone()).await.unwrap();
1731 let result2 = resolver.resolve_check(request).await.unwrap();
1732
1733 assert_eq!(result1, CheckResult::Allowed);
1734 assert_eq!(result2, CheckResult::Allowed);
1735 }
1736
1737 #[tokio::test]
1738 async fn test_result_cache_bypass_with_contextual_tuples() {
1739 let dsl = "type document { relations define viewer: [user] }";
1740 let model = parse_dsl(dsl).unwrap();
1741 let ts = TypeSystem::new(model);
1742
1743 let datastore = MockDatastore { tuples: vec![] };
1744 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1745
1746 let mut request = ResolveCheckRequest::new(
1747 "document".into(),
1748 "doc1".into(),
1749 "viewer".into(),
1750 "user".into(),
1751 "alice".into(),
1752 );
1753 request.contextual_tuples = vec![Tuple {
1754 object_type: "document".to_string(),
1755 object_id: "doc1".to_string(),
1756 relation: "viewer".to_string(),
1757 subject_type: "user".to_string(),
1758 subject_id: "alice".to_string(),
1759 condition: None,
1760 }];
1761
1762 let result = resolver.resolve_check(request).await.unwrap();
1763 assert_eq!(result, CheckResult::Allowed);
1764 }
1765
1766 #[tokio::test]
1767 async fn test_result_cache_key_differs_for_different_subjects() {
1768 let dsl = "type document { relations define viewer: [user] }";
1769 let model = parse_dsl(dsl).unwrap();
1770 let ts = TypeSystem::new(model);
1771
1772 let tuples = vec![Tuple {
1773 object_type: "document".to_string(),
1774 object_id: "doc1".to_string(),
1775 relation: "viewer".to_string(),
1776 subject_type: "user".to_string(),
1777 subject_id: "alice".to_string(),
1778 condition: None,
1779 }];
1780
1781 let datastore = MockDatastore { tuples };
1782 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1783
1784 let request_alice = ResolveCheckRequest::new(
1785 "document".into(),
1786 "doc1".into(),
1787 "viewer".into(),
1788 "user".into(),
1789 "alice".into(),
1790 );
1791 let request_bob = ResolveCheckRequest::new(
1792 "document".into(),
1793 "doc1".into(),
1794 "viewer".into(),
1795 "user".into(),
1796 "bob".into(),
1797 );
1798
1799 let result_alice = resolver.resolve_check(request_alice).await.unwrap();
1800 let result_bob = resolver.resolve_check(request_bob).await.unwrap();
1801
1802 assert_eq!(result_alice, CheckResult::Allowed);
1803 assert_eq!(result_bob, CheckResult::Denied);
1804 }
1805
1806 #[tokio::test]
1809 async fn test_tuple_cache_hit() {
1810 let dsl = "type document { relations define viewer: [user] }";
1811 let model = parse_dsl(dsl).unwrap();
1812 let ts = TypeSystem::new(model);
1813
1814 let tuples = vec![Tuple {
1815 object_type: "document".to_string(),
1816 object_id: "doc1".to_string(),
1817 relation: "viewer".to_string(),
1818 subject_type: "user".to_string(),
1819 subject_id: "alice".to_string(),
1820 condition: None,
1821 }];
1822
1823 let datastore = MockDatastore { tuples };
1824 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1825
1826 let request = ResolveCheckRequest::new(
1827 "document".into(),
1828 "doc1".into(),
1829 "viewer".into(),
1830 "user".into(),
1831 "alice".into(),
1832 );
1833
1834 let _result1 = resolver.resolve_check(request.clone()).await.unwrap();
1835 let result2 = resolver.resolve_check(request).await.unwrap();
1836
1837 assert_eq!(result2, CheckResult::Allowed);
1838 }
1839
1840 #[tokio::test]
1841 async fn test_tuple_cache_bypass_with_contextual() {
1842 let dsl = "type document { relations define viewer: [user] }";
1843 let model = parse_dsl(dsl).unwrap();
1844 let ts = TypeSystem::new(model);
1845
1846 let datastore = MockDatastore { tuples: vec![] };
1847 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1848
1849 let mut request = ResolveCheckRequest::new(
1850 "document".into(),
1851 "doc1".into(),
1852 "viewer".into(),
1853 "user".into(),
1854 "alice".into(),
1855 );
1856 request.contextual_tuples = vec![Tuple {
1857 object_type: "document".to_string(),
1858 object_id: "doc1".to_string(),
1859 relation: "viewer".to_string(),
1860 subject_type: "user".to_string(),
1861 subject_id: "alice".to_string(),
1862 condition: None,
1863 }];
1864
1865 let result = resolver.resolve_check(request).await.unwrap();
1866 assert_eq!(result, CheckResult::Allowed);
1867 }
1868
1869 #[tokio::test]
1872 async fn test_unknown_relation_returns_error() {
1873 let dsl = "type document { relations define viewer: [user] }";
1874 let model = parse_dsl(dsl).unwrap();
1875 let ts = TypeSystem::new(model);
1876
1877 let datastore = MockDatastore { tuples: vec![] };
1878 let resolver = CoreResolver::new(datastore, StaticPolicyProvider::new(ts));
1879
1880 let request = ResolveCheckRequest::new(
1881 "document".into(),
1882 "doc1".into(),
1883 "unknown_relation".into(),
1884 "user".into(),
1885 "alice".into(),
1886 );
1887
1888 let result = resolver.resolve_check(request).await;
1889 assert!(result.is_err());
1890 assert!(matches!(
1891 result.unwrap_err(),
1892 AuthzError::RelationNotFound { .. }
1893 ));
1894 }
1895}