1#![allow(clippy::module_name_repetitions)]
21use super::check_parse::CheckParseAnswer;
22#[cfg(feature = "partial-eval")]
23use super::utils::JsonValueWithNoDuplicateKeys;
24use super::utils::{Context, DetailedError, Entities, EntityUid, PolicySet, Schema, WithWarnings};
25use crate::{Authorizer, Decision, PolicyId, Request};
26use cedar_policy_core::validator::cedar_schema::SchemaWarning;
27use serde::{Deserialize, Serialize};
28use serde_with::serde_as;
29use std::collections::HashMap;
30use std::collections::HashSet;
31#[cfg(feature = "wasm")]
32use wasm_bindgen::prelude::wasm_bindgen;
33
34#[cfg(feature = "wasm")]
35extern crate tsify;
36
37thread_local!(
38 static AUTHORIZER: Authorizer = Authorizer::new();
40 static PREPARSED_POLICY_SETS: std::cell::RefCell<HashMap<String, crate::PolicySet>> =
44 std::cell::RefCell::new(HashMap::new());
45 static PREPARSED_SCHEMAS: std::cell::RefCell<HashMap<String, crate::Schema>> =
49 std::cell::RefCell::new(HashMap::new());
50);
51
52#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "isAuthorized"))]
54pub fn is_authorized(call: AuthorizationCall) -> AuthorizationAnswer {
55 match call.parse() {
56 WithWarnings {
57 t: Ok((request, policies, entities)),
58 warnings,
59 } => AuthorizationAnswer::Success {
60 response: AUTHORIZER.with(|authorizer| {
61 authorizer
62 .is_authorized(&request, &policies, &entities)
63 .into()
64 }),
65 warnings: warnings.into_iter().map(Into::into).collect(),
66 },
67 WithWarnings {
68 t: Err(errors),
69 warnings,
70 } => AuthorizationAnswer::Failure {
71 errors: errors.into_iter().map(Into::into).collect(),
72 warnings: warnings.into_iter().map(Into::into).collect(),
73 },
74 }
75}
76
77pub fn is_authorized_json(json: serde_json::Value) -> Result<serde_json::Value, serde_json::Error> {
85 let ans = is_authorized(serde_json::from_value(json)?);
86 serde_json::to_value(ans)
87}
88
89pub fn is_authorized_json_str(json: &str) -> Result<String, serde_json::Error> {
97 let ans = is_authorized(serde_json::from_str(json)?);
98 serde_json::to_string(&ans)
99}
100
101#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "preparsePolicySet"))]
107pub fn preparse_policy_set(pset_id: String, policies: PolicySet) -> CheckParseAnswer {
108 use super::check_parse::CheckParseAnswer;
109
110 match policies.parse() {
112 Ok(parsed_policies) => {
113 PREPARSED_POLICY_SETS.with(|cache| {
114 cache.borrow_mut().insert(pset_id, parsed_policies);
115 });
116 CheckParseAnswer::Success
117 }
118 Err(errors) => CheckParseAnswer::Failure {
119 errors: errors.into_iter().map(Into::into).collect(),
120 },
121 }
122}
123
124#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "preparseSchema"))]
130pub fn preparse_schema(schema_name: String, schema: Schema) -> CheckParseAnswer {
131 use super::check_parse::CheckParseAnswer;
132
133 match schema.parse() {
135 Ok((parsed_schema, _warnings)) => {
136 PREPARSED_SCHEMAS.with(|cache| {
137 cache.borrow_mut().insert(schema_name, parsed_schema);
138 });
139 CheckParseAnswer::Success
140 }
141 Err(error) => CheckParseAnswer::Failure {
142 errors: vec![error.into()],
143 },
144 }
145}
146
147#[doc = include_str!("../../experimental_warning.md")]
150#[cfg(feature = "partial-eval")]
151pub fn is_authorized_partial(call: PartialAuthorizationCall) -> PartialAuthorizationAnswer {
152 match call.parse() {
153 WithWarnings {
154 t: Ok((request, policies, entities)),
155 warnings,
156 } => {
157 let response = AUTHORIZER.with(|authorizer| {
158 authorizer.is_authorized_partial(&request, &policies, &entities)
159 });
160 let warnings = warnings.into_iter().map(Into::into).collect();
161 match ResidualResponse::try_from(response) {
162 Ok(response) => PartialAuthorizationAnswer::Residuals {
163 response: Box::new(response),
164 warnings,
165 },
166 Err(e) => PartialAuthorizationAnswer::Failure {
167 errors: vec![miette::Report::new_boxed(e).into()],
168 warnings,
169 },
170 }
171 }
172 WithWarnings {
173 t: Err(errors),
174 warnings,
175 } => PartialAuthorizationAnswer::Failure {
176 errors: errors.into_iter().map(Into::into).collect(),
177 warnings: warnings.into_iter().map(Into::into).collect(),
178 },
179 }
180}
181
182#[doc = include_str!("../../experimental_warning.md")]
190#[cfg(feature = "partial-eval")]
191pub fn is_authorized_partial_json(
192 json: serde_json::Value,
193) -> Result<serde_json::Value, serde_json::Error> {
194 let ans = is_authorized_partial(serde_json::from_value(json)?);
195 serde_json::to_value(ans)
196}
197
198#[doc = include_str!("../../experimental_warning.md")]
206#[cfg(feature = "partial-eval")]
207pub fn is_authorized_partial_json_str(json: &str) -> Result<String, serde_json::Error> {
208 let ans = is_authorized_partial(serde_json::from_str(json)?);
209 serde_json::to_string(&ans)
210}
211
212#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
214#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
215#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
216#[serde(rename_all = "camelCase")]
217#[serde(deny_unknown_fields)]
218pub struct Response {
219 decision: Decision,
221 diagnostics: Diagnostics,
223}
224
225#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
228#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
229#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
230#[serde(rename_all = "camelCase")]
231#[serde(deny_unknown_fields)]
232pub struct Diagnostics {
233 reason: HashSet<PolicyId>,
236 errors: HashSet<AuthorizationError>,
238}
239
240impl Response {
241 pub fn new(
243 decision: Decision,
244 reason: HashSet<PolicyId>,
245 errors: HashSet<AuthorizationError>,
246 ) -> Self {
247 Self {
248 decision,
249 diagnostics: Diagnostics { reason, errors },
250 }
251 }
252
253 pub fn decision(&self) -> Decision {
255 self.decision
256 }
257
258 pub fn diagnostics(&self) -> &Diagnostics {
260 &self.diagnostics
261 }
262}
263
264impl From<crate::Response> for Response {
265 fn from(response: crate::Response) -> Self {
266 let (reason, errors) = response.diagnostics.into_components();
267 Self::new(
268 response.decision,
269 reason.collect(),
270 errors.map(Into::into).collect(),
271 )
272 }
273}
274
275#[cfg(feature = "partial-eval")]
276impl From<crate::PartialResponse> for Response {
277 fn from(partial_response: crate::PartialResponse) -> Self {
278 partial_response.concretize().into()
279 }
280}
281
282impl Diagnostics {
283 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
285 self.reason.iter()
286 }
287
288 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
290 self.errors.iter()
291 }
292}
293
294#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
296#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
297#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
298#[serde(rename_all = "camelCase")]
299#[serde(deny_unknown_fields)]
300pub struct AuthorizationError {
301 #[cfg_attr(feature = "wasm", tsify(type = "string"))]
303 pub policy_id: PolicyId,
304 pub error: DetailedError,
308}
309
310impl AuthorizationError {
311 pub fn new(
313 policy_id: impl Into<PolicyId>,
314 error: impl miette::Diagnostic + Send + Sync + 'static,
315 ) -> Self {
316 Self::new_from_report(policy_id, miette::Report::new(error))
317 }
318
319 pub fn new_from_report(policy_id: impl Into<PolicyId>, report: miette::Report) -> Self {
321 Self {
322 policy_id: policy_id.into(),
323 error: report.into(),
324 }
325 }
326}
327
328impl From<crate::AuthorizationError> for AuthorizationError {
329 fn from(e: crate::AuthorizationError) -> Self {
330 match e {
331 crate::AuthorizationError::PolicyEvaluationError(e) => {
332 Self::new(e.policy_id().clone(), e.into_inner())
333 }
334 }
335 }
336}
337
338#[doc(hidden)]
339impl From<cedar_policy_core::authorizer::AuthorizationError> for AuthorizationError {
340 fn from(e: cedar_policy_core::authorizer::AuthorizationError) -> Self {
341 crate::AuthorizationError::from(e).into()
342 }
343}
344
345#[doc = include_str!("../../experimental_warning.md")]
347#[cfg(feature = "partial-eval")]
348#[derive(Debug, Clone, Serialize, Deserialize)]
349#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
350#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
351#[serde(rename_all = "camelCase")]
352#[serde(deny_unknown_fields)]
353pub struct ResidualResponse {
354 decision: Option<Decision>,
355 satisfied: HashSet<PolicyId>,
356 errored: HashSet<PolicyId>,
357 may_be_determining: HashSet<PolicyId>,
358 must_be_determining: HashSet<PolicyId>,
359 residuals: HashMap<PolicyId, JsonValueWithNoDuplicateKeys>,
360 nontrivial_residuals: HashSet<PolicyId>,
361}
362
363#[cfg(feature = "partial-eval")]
364impl ResidualResponse {
365 pub fn decision(&self) -> Option<Decision> {
367 self.decision
368 }
369
370 pub fn satisfied(&self) -> impl Iterator<Item = &PolicyId> {
372 self.satisfied.iter()
373 }
374
375 pub fn errored(&self) -> impl Iterator<Item = &PolicyId> {
377 self.errored.iter()
378 }
379
380 pub fn may_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
382 self.may_be_determining.iter()
383 }
384
385 pub fn must_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
387 self.must_be_determining.iter()
388 }
389
390 pub fn residuals(&self) -> impl Iterator<Item = &JsonValueWithNoDuplicateKeys> {
392 self.residuals.values()
393 }
394
395 pub fn into_residuals(self) -> impl Iterator<Item = JsonValueWithNoDuplicateKeys> {
397 self.residuals.into_values()
398 }
399
400 pub fn residual(&self, p: &PolicyId) -> Option<&JsonValueWithNoDuplicateKeys> {
402 self.residuals.get(p)
403 }
404
405 pub fn nontrivial_residuals(&self) -> impl Iterator<Item = &JsonValueWithNoDuplicateKeys> {
407 self.residuals.iter().filter_map(|(id, policy)| {
408 if self.nontrivial_residuals.contains(id) {
409 Some(policy)
410 } else {
411 None
412 }
413 })
414 }
415
416 pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &PolicyId> {
418 self.nontrivial_residuals.iter()
419 }
420}
421
422#[cfg(feature = "partial-eval")]
423impl TryFrom<crate::PartialResponse> for ResidualResponse {
424 type Error = Box<dyn miette::Diagnostic + Send + Sync + 'static>;
425
426 fn try_from(partial_response: crate::PartialResponse) -> Result<Self, Self::Error> {
427 Ok(Self {
428 decision: partial_response.decision(),
429 satisfied: partial_response
430 .definitely_satisfied()
431 .map(|p| p.id().clone())
432 .collect(),
433 errored: partial_response.definitely_errored().cloned().collect(),
434 may_be_determining: partial_response
435 .may_be_determining()
436 .map(|p| p.id().clone())
437 .collect(),
438 must_be_determining: partial_response
439 .must_be_determining()
440 .map(|p| p.id().clone())
441 .collect(),
442 nontrivial_residuals: partial_response
443 .nontrivial_residuals()
444 .map(|p| p.id().clone())
445 .collect(),
446 residuals: partial_response
447 .all_residuals()
448 .map(|e| e.to_json().map(|json| (e.id().clone(), json.into())))
449 .collect::<Result<_, _>>()?,
450 })
451 }
452}
453
454#[derive(Debug, Serialize, Deserialize)]
456#[serde(tag = "type")]
457#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
458#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
459#[serde(rename_all = "camelCase")]
460pub enum AuthorizationAnswer {
461 #[serde(rename_all = "camelCase")]
463 Failure {
464 errors: Vec<DetailedError>,
466 warnings: Vec<DetailedError>,
468 },
469 #[serde(rename_all = "camelCase")]
472 Success {
473 response: Response,
476 warnings: Vec<DetailedError>,
481 },
482}
483
484#[cfg(feature = "partial-eval")]
486#[derive(Debug, Serialize, Deserialize)]
487#[serde(tag = "type")]
488#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
489#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
490#[serde(rename_all = "camelCase")]
491pub enum PartialAuthorizationAnswer {
492 #[serde(rename_all = "camelCase")]
494 Failure {
495 errors: Vec<DetailedError>,
497 warnings: Vec<DetailedError>,
499 },
500 #[serde(rename_all = "camelCase")]
503 Residuals {
504 response: Box<ResidualResponse>,
506 warnings: Vec<DetailedError>,
511 },
512}
513
514#[serde_as]
516#[derive(Debug, Serialize, Deserialize)]
517#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
518#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
519#[serde(rename_all = "camelCase")]
520#[serde(deny_unknown_fields)]
521pub struct AuthorizationCall {
522 principal: EntityUid,
524 action: EntityUid,
526 resource: EntityUid,
528 context: Context,
530 #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
535 schema: Option<Schema>,
536 #[serde(default = "constant_true")]
541 validate_request: bool,
542 policies: PolicySet,
544 entities: Entities,
546}
547
548#[serde_as]
550#[derive(Debug, Serialize, Deserialize)]
551#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
552#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
553#[serde(rename_all = "camelCase")]
554#[serde(deny_unknown_fields)]
555pub struct StatefulAuthorizationCall {
556 principal: EntityUid,
558 action: EntityUid,
560 resource: EntityUid,
562 context: Context,
564 #[cfg_attr(feature = "wasm", tsify(optional, type = "string"))]
569 preparsed_schema_name: Option<String>,
570 #[serde(default = "constant_true")]
575 validate_request: bool,
576 preparsed_policy_set_id: String,
578 entities: Entities,
580}
581
582#[cfg(feature = "partial-eval")]
584#[serde_as]
585#[derive(Debug, Serialize, Deserialize)]
586#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
587#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
588#[serde(rename_all = "camelCase")]
589#[serde(deny_unknown_fields)]
590pub struct PartialAuthorizationCall {
591 principal: Option<EntityUid>,
593 action: Option<EntityUid>,
595 resource: Option<EntityUid>,
597 context: Context,
599 #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
604 schema: Option<Schema>,
605 #[serde(default = "constant_true")]
610 validate_request: bool,
611 policies: PolicySet,
613 entities: Entities,
615}
616
617#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "statefulIsAuthorized"))]
622pub fn stateful_is_authorized(call: StatefulAuthorizationCall) -> AuthorizationAnswer {
623 match call.parse() {
624 WithWarnings {
625 t: Ok((request, policies, entities)),
626 warnings,
627 } => AuthorizationAnswer::Success {
628 response: AUTHORIZER.with(|authorizer| {
629 authorizer
630 .is_authorized(&request, &policies, &entities)
631 .into()
632 }),
633 warnings: warnings.into_iter().map(Into::into).collect(),
634 },
635 WithWarnings {
636 t: Err(errors),
637 warnings,
638 } => AuthorizationAnswer::Failure {
639 errors: errors.into_iter().map(Into::into).collect(),
640 warnings: warnings.into_iter().map(Into::into).collect(),
641 },
642 }
643}
644
645fn constant_true() -> bool {
646 true
647}
648
649fn build_error<T>(
650 errs: Vec<miette::Report>,
651 warnings: Vec<SchemaWarning>,
652) -> WithWarnings<Result<T, Vec<miette::Report>>> {
653 WithWarnings {
654 t: Err(errs),
655 warnings: warnings.into_iter().map(Into::into).collect(),
656 }
657}
658
659impl AuthorizationCall {
660 fn parse(
661 self,
662 ) -> WithWarnings<Result<(Request, crate::PolicySet, crate::Entities), Vec<miette::Report>>>
663 {
664 let mut errs = vec![];
665 let mut warnings = vec![];
666 let maybe_schema = self
667 .schema
668 .map(|schema| {
669 schema.parse().map(|(schema, new_warnings)| {
670 warnings.extend(new_warnings);
671 schema
672 })
673 })
674 .transpose()
675 .map_err(|e| errs.push(e));
676 let maybe_principal = self
677 .principal
678 .parse(Some("principal"))
679 .map_err(|e| errs.push(e));
680 let maybe_action = self.action.parse(Some("action")).map_err(|e| errs.push(e));
681 let maybe_resource = self
682 .resource
683 .parse(Some("resource"))
684 .map_err(|e| errs.push(e));
685
686 let (Ok(schema), Ok(principal), Ok(action), Ok(resource)) =
687 (maybe_schema, maybe_principal, maybe_action, maybe_resource)
688 else {
689 return build_error(errs, warnings);
691 };
692
693 let context = match self.context.parse(schema.as_ref(), Some(&action)) {
694 Ok(context) => context,
695 Err(e) => {
696 return build_error(vec![e], warnings);
697 }
698 };
699
700 let schema_opt = if self.validate_request {
701 schema.as_ref()
702 } else {
703 None
704 };
705 let maybe_request = Request::new(principal, action, resource, context, schema_opt)
706 .map_err(|e| errs.push(e.into()));
707 let maybe_entities = self
708 .entities
709 .parse(schema.as_ref())
710 .map_err(|e| errs.push(e));
711 let maybe_policies = self.policies.parse().map_err(|es| errs.extend(es));
712
713 match (maybe_request, maybe_policies, maybe_entities) {
714 (Ok(request), Ok(policies), Ok(entities)) => WithWarnings {
715 t: Ok((request, policies, entities)),
716 warnings: warnings.into_iter().map(Into::into).collect(),
717 },
718 _ => {
719 build_error(errs, warnings)
721 }
722 }
723 }
724}
725
726impl StatefulAuthorizationCall {
727 fn parse(
730 self,
731 ) -> WithWarnings<Result<(Request, crate::PolicySet, crate::Entities), Vec<miette::Report>>>
732 {
733 let mut errs = vec![];
734 let warnings = vec![];
735
736 let maybe_schema: Result<Option<crate::Schema>, ()> =
738 self.preparsed_schema_name.map_or_else(
739 || Ok(None),
740 |schema_name| {
741 PREPARSED_SCHEMAS
742 .with(|cache| cache.borrow().get(&schema_name).cloned())
743 .map_or_else(
744 || {
745 errs.push(miette::miette!(
746 "preparsed schema '{}' not found",
747 schema_name
748 ));
749 Ok(None)
750 },
751 |schema| Ok(Some(schema)),
752 )
753 },
754 );
755
756 let maybe_policies: Result<crate::PolicySet, ()> = if let Some(policies) =
758 PREPARSED_POLICY_SETS
759 .with(|cache| cache.borrow().get(&self.preparsed_policy_set_id).cloned())
760 {
761 Ok(policies)
762 } else {
763 errs.push(miette::miette!(
764 "preparsed policy set '{}' not found",
765 self.preparsed_policy_set_id
766 ));
767 Err(())
768 };
769
770 let maybe_principal = self
772 .principal
773 .parse(Some("principal"))
774 .map_err(|e| errs.push(e));
775 let maybe_action = self.action.parse(Some("action")).map_err(|e| errs.push(e));
776 let maybe_resource = self
777 .resource
778 .parse(Some("resource"))
779 .map_err(|e| errs.push(e));
780
781 let (Ok(schema), Ok(policies), Ok(principal), Ok(action), Ok(resource)) = (
783 maybe_schema,
784 maybe_policies,
785 maybe_principal,
786 maybe_action,
787 maybe_resource,
788 ) else {
789 return build_error(errs, warnings);
790 };
791
792 let context = match self.context.parse(schema.as_ref(), Some(&action)) {
794 Ok(context) => context,
795 Err(e) => {
796 return build_error(vec![e], warnings);
797 }
798 };
799
800 let schema_opt = if self.validate_request {
802 schema.as_ref()
803 } else {
804 None
805 };
806 let maybe_request = Request::new(principal, action, resource, context, schema_opt)
807 .map_err(|e| errs.push(e.into()));
808 let maybe_entities = self
809 .entities
810 .parse(schema.as_ref())
811 .map_err(|e| errs.push(e));
812
813 match (maybe_request, maybe_entities) {
815 (Ok(request), Ok(entities)) => WithWarnings {
816 t: Ok((request, policies, entities)),
817 warnings: warnings.into_iter().map(Into::into).collect(),
818 },
819 _ => build_error(errs, warnings),
820 }
821 }
822}
823
824#[cfg(feature = "partial-eval")]
825impl PartialAuthorizationCall {
826 fn parse(
827 self,
828 ) -> WithWarnings<Result<(Request, crate::PolicySet, crate::Entities), Vec<miette::Report>>>
829 {
830 let mut errs = vec![];
831 let mut warnings = vec![];
832 let maybe_schema = self
833 .schema
834 .map(|schema| {
835 schema.parse().map(|(schema, new_warnings)| {
836 warnings.extend(new_warnings);
837 schema
838 })
839 })
840 .transpose()
841 .map_err(|e| errs.push(e));
842 let maybe_principal = self
843 .principal
844 .map(|uid| uid.parse(Some("principal")))
845 .transpose()
846 .map_err(|e| errs.push(e));
847 let maybe_action = self
848 .action
849 .map(|uid| uid.parse(Some("action")))
850 .transpose()
851 .map_err(|e| errs.push(e));
852 let maybe_resource = self
853 .resource
854 .map(|uid| uid.parse(Some("resource")))
855 .transpose()
856 .map_err(|e| errs.push(e));
857
858 let (Ok(schema), Ok(principal), Ok(action), Ok(resource)) =
859 (maybe_schema, maybe_principal, maybe_action, maybe_resource)
860 else {
861 return build_error(errs, warnings);
863 };
864
865 let context = match self.context.parse(schema.as_ref(), action.as_ref()) {
866 Ok(context) => context,
867 Err(e) => {
868 return build_error(vec![e], warnings);
869 }
870 };
871
872 let maybe_entities = self
873 .entities
874 .parse(schema.as_ref())
875 .map_err(|e| errs.push(e));
876 let maybe_policies = self.policies.parse().map_err(|es| errs.extend(es));
877
878 let mut b = Request::builder();
879 if let Some(p) = principal {
880 b = b.principal(p);
881 }
882 if let Some(a) = action {
883 b = b.action(a);
884 }
885 if let Some(r) = resource {
886 b = b.resource(r);
887 }
888 b = b.context(context);
889
890 let maybe_request = match schema {
891 Some(schema) if self.validate_request => {
892 b.schema(&schema).build().map_err(|e| errs.push(e.into()))
893 }
894 _ => Ok(b.build()),
895 };
896
897 match (maybe_request, maybe_policies, maybe_entities) {
898 (Ok(request), Ok(policies), Ok(entities)) => WithWarnings {
899 t: Ok((request, policies, entities)),
900 warnings: warnings.into_iter().map(Into::into).collect(),
901 },
902 _ => {
903 build_error(errs, warnings)
905 }
906 }
907 }
908}
909
910#[allow(clippy::panic)]
912#[cfg(test)]
913mod test {
914 use super::*;
915
916 use crate::ffi::test_utils::*;
917 use cool_asserts::assert_matches;
918 use serde_json::json;
919
920 #[track_caller]
922 fn assert_is_authorized_json(json: serde_json::Value) {
923 let ans_val =
924 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
925 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
926 assert_matches!(result, Ok(AuthorizationAnswer::Success { response, .. }) => {
927 assert_eq!(response.decision(), Decision::Allow);
928 let errors: Vec<&AuthorizationError> = response.diagnostics().errors().collect();
929 assert_eq!(errors.len(), 0, "{errors:?}");
930 });
931 }
932
933 #[track_caller]
935 fn assert_is_not_authorized_json(json: serde_json::Value) {
936 let ans_val =
937 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
938 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
939 assert_matches!(result, Ok(AuthorizationAnswer::Success { response, .. }) => {
940 assert_eq!(response.decision(), Decision::Deny);
941 let errors: Vec<&AuthorizationError> = response.diagnostics().errors().collect();
942 assert_eq!(errors.len(), 0, "{errors:?}");
943 });
944 }
945
946 #[track_caller]
949 fn assert_is_authorized_json_str_is_failure(call: &str, msg: &str) {
950 assert_matches!(is_authorized_json_str(call), Err(e) => {
951 assert_eq!(e.to_string(), msg);
952 });
953 }
954
955 #[track_caller]
958 fn assert_is_authorized_json_is_failure(json: serde_json::Value) -> Vec<DetailedError> {
959 let ans_val =
960 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
961 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
962 assert_matches!(result, Ok(AuthorizationAnswer::Failure { errors, .. }) => errors)
963 }
964
965 #[test]
966 fn test_failure_on_invalid_syntax() {
967 assert_is_authorized_json_str_is_failure(
968 "iefjieoafiaeosij",
969 "expected value at line 1 column 1",
970 );
971 }
972
973 #[test]
974 fn test_not_authorized_on_empty_slice() {
975 let call = json!({
976 "principal": {
977 "type": "User",
978 "id": "alice"
979 },
980 "action": {
981 "type": "Photo",
982 "id": "view"
983 },
984 "resource": {
985 "type": "Photo",
986 "id": "door"
987 },
988 "context": {},
989 "policies": {},
990 "entities": []
991 });
992 assert_is_not_authorized_json(call);
993 }
994
995 #[test]
996 fn test_not_authorized_on_unspecified() {
997 let call = json!({
998 "principal": null,
999 "action": {
1000 "type": "Photo",
1001 "id": "view"
1002 },
1003 "resource": {
1004 "type": "Photo",
1005 "id": "door"
1006 },
1007 "context": {},
1008 "policies": {
1009 "staticPolicies": {
1010 "ID1": "permit(principal == User::\"alice\", action, resource);"
1011 }
1012 },
1013 "entities": []
1014 });
1015 let errs = assert_is_authorized_json_is_failure(call);
1017 assert_exactly_one_error(
1018 &errs,
1019 "failed to parse principal: in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
1020 Some("literal entity references can be made with `{ \"type\": \"SomeType\", \"id\": \"SomeId\" }`"),
1021 );
1022 }
1023
1024 #[test]
1025 fn test_authorized_on_simple_slice() {
1026 let call = json!({
1027 "principal": {
1028 "type": "User",
1029 "id": "alice"
1030 },
1031 "action": {
1032 "type": "Photo",
1033 "id": "view"
1034 },
1035 "resource": {
1036 "type": "Photo",
1037 "id": "door"
1038 },
1039 "context": {},
1040 "policies": {
1041 "staticPolicies": {
1042 "ID1": "permit(principal == User::\"alice\", action, resource);"
1043 }
1044 },
1045 "entities": []
1046 });
1047 assert_is_authorized_json(call);
1048 }
1049
1050 #[test]
1051 fn test_authorized_on_simple_slice_with_string_policies() {
1052 let call = json!({
1053 "principal": {
1054 "type": "User",
1055 "id": "alice"
1056 },
1057 "action": {
1058 "type": "Photo",
1059 "id": "view"
1060 },
1061 "resource": {
1062 "type": "Photo",
1063 "id": "door"
1064 },
1065 "context": {},
1066 "policies": {
1067 "staticPolicies": "permit(principal == User::\"alice\", action, resource);"
1068 },
1069 "entities": []
1070 });
1071 assert_is_authorized_json(call);
1072 }
1073
1074 #[test]
1075 fn test_authorized_on_simple_slice_with_context() {
1076 let call = json!({
1077 "principal": {
1078 "type": "User",
1079 "id": "alice"
1080 },
1081 "action": {
1082 "type": "Photo",
1083 "id": "view"
1084 },
1085 "resource": {
1086 "type": "Photo",
1087 "id": "door"
1088 },
1089 "context": {
1090 "is_authenticated": true,
1091 "source_ip": {
1092 "__extn" : { "fn" : "ip", "arg" : "222.222.222.222" }
1093 }
1094 },
1095 "policies": {
1096 "staticPolicies": "permit(principal == User::\"alice\", action, resource) when { context.is_authenticated && context.source_ip.isInRange(ip(\"222.222.222.0/24\")) };"
1097 },
1098 "entities": []
1099 });
1100 assert_is_authorized_json(call);
1101 }
1102
1103 #[test]
1104 #[cfg(feature = "variadic-is-in-range")]
1105 fn test_authorized_on_simple_slice_with_context_variadic() {
1106 let call = json!({
1107 "principal": {
1108 "type": "User",
1109 "id": "alice"
1110 },
1111 "action": {
1112 "type": "Photo",
1113 "id": "view"
1114 },
1115 "resource": {
1116 "type": "Photo",
1117 "id": "door"
1118 },
1119 "context": {
1120 "is_authenticated": true,
1121 "source_ip": {
1122 "__extn" : { "fn" : "ip", "arg" : "222.222.222.222" }
1123 }
1124 },
1125 "policies": {
1126 "staticPolicies": "permit(principal == User::\"alice\", action, resource) when { context.is_authenticated && context.source_ip.isInRange(ip(\"192.167.0.1/24\"), ip(\"222.222.222.0/24\")) };"
1127 },
1128 "entities": []
1129 });
1130 assert_is_authorized_json(call);
1131 }
1132
1133 #[test]
1134 fn test_authorized_on_simple_slice_with_attrs_and_parents() {
1135 let call = json!({
1136 "principal": {
1137 "type": "User",
1138 "id": "alice"
1139 },
1140 "action": {
1141 "type": "Photo",
1142 "id": "view"
1143 },
1144 "resource": {
1145 "type": "Photo",
1146 "id": "door"
1147 },
1148 "context": {},
1149 "policies": {
1150 "staticPolicies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };"
1151 },
1152 "entities": [
1153 {
1154 "uid": {
1155 "__entity": {
1156 "type": "User",
1157 "id": "alice"
1158 }
1159 },
1160 "attrs": {},
1161 "parents": []
1162 },
1163 {
1164 "uid": {
1165 "__entity": {
1166 "type": "Photo",
1167 "id": "door"
1168 }
1169 },
1170 "attrs": {
1171 "owner": {
1172 "__entity": {
1173 "type": "User",
1174 "id": "alice"
1175 }
1176 }
1177 },
1178 "parents": [
1179 {
1180 "__entity": {
1181 "type": "Folder",
1182 "id": "house"
1183 }
1184 }
1185 ]
1186 },
1187 {
1188 "uid": {
1189 "__entity": {
1190 "type": "Folder",
1191 "id": "house"
1192 }
1193 },
1194 "attrs": {},
1195 "parents": []
1196 }
1197 ]
1198 });
1199 assert_is_authorized_json(call);
1200 }
1201
1202 #[test]
1203 fn test_authorized_on_multi_policy_slice() {
1204 let call = json!({
1205 "principal": {
1206 "type": "User",
1207 "id": "alice"
1208 },
1209 "action": {
1210 "type": "Photo",
1211 "id": "view"
1212 },
1213 "resource": {
1214 "type": "Photo",
1215 "id": "door"
1216 },
1217 "context": {},
1218 "policies": {
1219 "staticPolicies": {
1220 "ID0": "permit(principal == User::\"jerry\", action, resource == Photo::\"doorx\");",
1221 "ID1": "permit(principal == User::\"tom\", action, resource == Photo::\"doory\");",
1222 "ID2": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");"
1223 }
1224 },
1225 "entities": []
1226 });
1227 assert_is_authorized_json(call);
1228 }
1229
1230 #[test]
1231 fn test_authorized_on_multi_policy_slice_with_string_policies() {
1232 let call = json!({
1233 "principal": {
1234 "type": "User",
1235 "id": "alice"
1236 },
1237 "action": {
1238 "type": "Photo",
1239 "id": "view"
1240 },
1241 "resource": {
1242 "type": "Photo",
1243 "id": "door"
1244 },
1245 "context": {},
1246 "policies": {
1247 "staticPolicies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };"
1248 },
1249 "entities": [
1250 {
1251 "uid": {
1252 "__entity": {
1253 "type": "User",
1254 "id": "alice"
1255 }
1256 },
1257 "attrs": {},
1258 "parents": []
1259 },
1260 {
1261 "uid": {
1262 "__entity": {
1263 "type": "Photo",
1264 "id": "door"
1265 }
1266 },
1267 "attrs": {
1268 "owner": {
1269 "__entity": {
1270 "type": "User",
1271 "id": "alice"
1272 }
1273 }
1274 },
1275 "parents": [
1276 {
1277 "__entity": {
1278 "type": "Folder",
1279 "id": "house"
1280 }
1281 }
1282 ]
1283 },
1284 {
1285 "uid": {
1286 "__entity": {
1287 "type": "Folder",
1288 "id": "house"
1289 }
1290 },
1291 "attrs": {},
1292 "parents": []
1293 }
1294 ]
1295 });
1296 assert_is_authorized_json(call);
1297 }
1298
1299 #[test]
1300 fn test_authorized_on_multi_policy_slice_denies_when_expected() {
1301 let call = json!({
1302 "principal": {
1303 "type": "User",
1304 "id": "alice"
1305 },
1306 "action": {
1307 "type": "Photo",
1308 "id": "view"
1309 },
1310 "resource": {
1311 "type": "Photo",
1312 "id": "door"
1313 },
1314 "context": {},
1315 "policies": {
1316 "staticPolicies": {
1317 "ID0": "permit(principal, action, resource);",
1318 "ID1": "forbid(principal == User::\"alice\", action, resource == Photo::\"door\");"
1319 }
1320 },
1321 "entities": []
1322 });
1323 assert_is_not_authorized_json(call);
1324 }
1325
1326 #[test]
1327 fn test_authorized_on_multi_policy_slice_with_string_policies_denies_when_expected() {
1328 let call = json!({
1329 "principal": {
1330 "type": "User",
1331 "id": "alice"
1332 },
1333 "action": {
1334 "type": "Photo",
1335 "id": "view"
1336 },
1337 "resource": {
1338 "type": "Photo",
1339 "id": "door"
1340 },
1341 "context": {},
1342 "policies": {
1343 "staticPolicies": "permit(principal, action, resource);\nforbid(principal == User::\"alice\", action, resource);"
1344 },
1345 "entities": []
1346 });
1347 assert_is_not_authorized_json(call);
1348 }
1349
1350 #[test]
1351 fn test_authorized_with_template_as_policy_should_fail() {
1352 let call = json!({
1353 "principal": {
1354 "type": "User",
1355 "id": "alice"
1356 },
1357 "action": {
1358 "type": "Photo",
1359 "id": "view"
1360 },
1361 "resource": {
1362 "type": "Photo",
1363 "id": "door"
1364 },
1365 "context": {},
1366 "policies": {
1367 "staticPolicies": "permit(principal == ?principal, action, resource);"
1368 },
1369 "entities": []
1370 });
1371 let errs = assert_is_authorized_json_is_failure(call);
1372 assert_exactly_one_error(&errs, "static policy set includes a template", None);
1373 }
1374
1375 #[test]
1376 fn test_authorized_with_template_should_fail() {
1377 let call = json!({
1378 "principal": {
1379 "type": "User",
1380 "id": "alice"
1381 },
1382 "action": {
1383 "type": "Photo",
1384 "id": "view"
1385 },
1386 "resource": {
1387 "type": "Photo",
1388 "id": "door"
1389 },
1390 "context": {},
1391 "policies": {
1392 "templates": {
1393 "ID0": "permit(principal == ?principal, action, resource);"
1394 }
1395 },
1396 "entities": [],
1397 });
1398 assert_is_not_authorized_json(call);
1399 }
1400
1401 #[test]
1402 fn test_authorized_with_template_link() {
1403 let call = json!({
1404 "principal": {
1405 "type": "User",
1406 "id": "alice"
1407 },
1408 "action": {
1409 "type": "Photo",
1410 "id": "view"
1411 },
1412 "resource": {
1413 "type": "Photo",
1414 "id": "door"
1415 },
1416 "context": {},
1417 "policies": {
1418 "templates": {
1419 "ID0": "permit(principal == ?principal, action, resource);"
1420 },
1421 "templateLinks": [
1422 {
1423 "templateId": "ID0",
1424 "newId": "ID0_User_alice",
1425 "values": {
1426 "?principal": { "type": "User", "id": "alice" }
1427 }
1428 }
1429 ]
1430 },
1431 "entities": []
1432 });
1433 assert_is_authorized_json(call);
1434 }
1435
1436 #[test]
1437 fn test_authorized_fails_on_policy_collision_with_template() {
1438 let call = json!({
1439 "principal" : {
1440 "type" : "User",
1441 "id" : "alice"
1442 },
1443 "action" : {
1444 "type" : "Action",
1445 "id" : "view"
1446 },
1447 "resource" : {
1448 "type" : "Photo",
1449 "id" : "door"
1450 },
1451 "context" : {},
1452 "policies": {
1453 "staticPolicies": {
1454 "ID0": "permit(principal, action, resource);"
1455 },
1456 "templates": {
1457 "ID0": "permit(principal == ?principal, action, resource);"
1458 }
1459 },
1460 "entities" : []
1461 });
1462 let errs = assert_is_authorized_json_is_failure(call);
1463 assert_exactly_one_error(
1464 &errs,
1465 "failed to add template with id `ID0` to policy set: duplicate template or policy id `ID0`",
1466 None,
1467 );
1468 }
1469
1470 #[test]
1471 fn test_authorized_fails_on_duplicate_link_ids() {
1472 let call = json!({
1473 "principal" : {
1474 "type" : "User",
1475 "id" : "alice"
1476 },
1477 "action" : {
1478 "type" : "Action",
1479 "id" : "view"
1480 },
1481 "resource" : {
1482 "type" : "Photo",
1483 "id" : "door"
1484 },
1485 "context" : {},
1486 "policies" : {
1487 "templates": {
1488 "ID0": "permit(principal == ?principal, action, resource);"
1489 },
1490 "templateLinks" : [
1491 {
1492 "templateId" : "ID0",
1493 "newId" : "ID1",
1494 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1495 },
1496 {
1497 "templateId" : "ID0",
1498 "newId" : "ID1",
1499 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1500 }
1501 ]
1502 },
1503 "entities" : [],
1504 });
1505 let errs = assert_is_authorized_json_is_failure(call);
1506 assert_exactly_one_error(
1507 &errs,
1508 "unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1509 None,
1510 );
1511 }
1512
1513 #[test]
1514 fn test_authorized_fails_on_template_link_collision_with_template() {
1515 let call = json!({
1516 "principal" : {
1517 "type" : "User",
1518 "id" : "alice"
1519 },
1520 "action" : {
1521 "type" : "Action",
1522 "id" : "view"
1523 },
1524 "resource" : {
1525 "type" : "Photo",
1526 "id" : "door"
1527 },
1528 "context" : {},
1529 "policies" : {
1530 "templates": {
1531 "ID0": "permit(principal == ?principal, action, resource);"
1532 },
1533 "templateLinks" : [
1534 {
1535 "templateId" : "ID0",
1536 "newId" : "ID0",
1537 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1538 }
1539 ]
1540 },
1541 "entities" : []
1542
1543 });
1544 let errs = assert_is_authorized_json_is_failure(call);
1545 assert_exactly_one_error(
1546 &errs,
1547 "unable to link template: template-linked policy id `ID0` conflicts with an existing policy id",
1548 None,
1549 );
1550 }
1551
1552 #[test]
1553 fn test_authorized_fails_on_template_link_collision_with_policy() {
1554 let call = json!({
1555 "principal" : {
1556 "type" : "User",
1557 "id" : "alice"
1558 },
1559 "action" : {
1560 "type" : "Action",
1561 "id" : "view"
1562 },
1563 "resource" : {
1564 "type" : "Photo",
1565 "id" : "door"
1566 },
1567 "context" : {},
1568 "policies" : {
1569 "staticPolicies" : {
1570 "ID1": "permit(principal, action, resource);"
1571 },
1572 "templates": {
1573 "ID0": "permit(principal == ?principal, action, resource);"
1574 },
1575 "templateLinks" : [
1576 {
1577 "templateId" : "ID0",
1578 "newId" : "ID1",
1579 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1580 }
1581 ]
1582 },
1583 "entities" : []
1584 });
1585 let errs = assert_is_authorized_json_is_failure(call);
1586 assert_exactly_one_error(
1587 &errs,
1588 "unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1589 None,
1590 );
1591 }
1592
1593 #[test]
1594 fn test_authorized_fails_on_duplicate_policy_ids() {
1595 let call = r#"{
1596 "principal" : {
1597 "type" : "User",
1598 "id" : "alice"
1599 },
1600 "action" : {
1601 "type" : "Action",
1602 "id" : "view"
1603 },
1604 "resource" : {
1605 "type" : "Photo",
1606 "id" : "door"
1607 },
1608 "context" : {},
1609 "policies" : {
1610 "staticPolicies" : {
1611 "ID0": "permit(principal, action, resource);",
1612 "ID0": "permit(principal, action, resource);"
1613 }
1614 },
1615 "entities" : [],
1616 }"#;
1617 assert_is_authorized_json_str_is_failure(
1618 call,
1619 "expected a static policy set represented by a string, JSON array, or JSON object (with no duplicate keys) at line 20 column 13",
1620 );
1621 }
1622
1623 #[test]
1624 fn test_authorized_fails_on_duplicate_template_ids() {
1625 let call = r#"{
1626 "principal" : {
1627 "type" : "User",
1628 "id" : "alice"
1629 },
1630 "action" : {
1631 "type" : "Action",
1632 "id" : "view"
1633 },
1634 "resource" : {
1635 "type" : "Photo",
1636 "id" : "door"
1637 },
1638 "context" : {},
1639 "policies" : {
1640 "templates" : {
1641 "ID0": "permit(principal == ?principal, action, resource);",
1642 "ID0": "permit(principal == ?principal, action, resource);"
1643 }
1644 },
1645 "entities" : []
1646 }"#;
1647 assert_is_authorized_json_str_is_failure(
1648 call,
1649 "invalid entry: found duplicate key at line 19 column 17",
1650 );
1651 }
1652
1653 #[test]
1654 fn test_authorized_fails_on_duplicate_slot_link() {
1655 let call = r#"{
1656 "principal" : {
1657 "type" : "User",
1658 "id" : "alice"
1659 },
1660 "action" : {
1661 "type" : "Action",
1662 "id" : "view"
1663 },
1664 "resource" : {
1665 "type" : "Photo",
1666 "id" : "door"
1667 },
1668 "context" : {},
1669 "policies" : {
1670 "templates" : {
1671 "ID0": "permit(principal == ?principal, action, resource);"
1672 },
1673 "templateLinks" : [{
1674 "templateId" : "ID0",
1675 "newId" : "ID1",
1676 "values" : {
1677 "?principal": { "type" : "User", "id" : "alice" },
1678 "?principal": { "type" : "User", "id" : "alice" }
1679 }
1680 }]
1681 },
1682 "entities" : [],
1683 }"#;
1684 assert_is_authorized_json_str_is_failure(
1685 call,
1686 "invalid entry: found duplicate key at line 25 column 21",
1687 );
1688 }
1689
1690 #[test]
1691 fn test_authorized_fails_inconsistent_duplicate_entity_uid() {
1692 let call = json!({
1693 "principal" : {
1694 "type" : "User",
1695 "id" : "alice"
1696 },
1697 "action" : {
1698 "type" : "Photo",
1699 "id" : "view"
1700 },
1701 "resource" : {
1702 "type" : "Photo",
1703 "id" : "door"
1704 },
1705 "context" : {},
1706 "policies" : {},
1707 "entities" : [
1708 {
1709 "uid": {
1710 "type" : "User",
1711 "id" : "alice"
1712 },
1713 "attrs": {"location": "Greenland"},
1714 "parents": []
1715 },
1716 {
1717 "uid": {
1718 "type" : "User",
1719 "id" : "alice"
1720 },
1721 "attrs": {},
1722 "parents": []
1723 }
1724 ]
1725 });
1726 let errs = assert_is_authorized_json_is_failure(call);
1727 assert_exactly_one_error(&errs, r#"duplicate entity entry `User::"alice"`"#, None);
1728 }
1729
1730 #[test]
1731 fn test_authorized_fails_duplicate_context_key() {
1732 let call = r#"{
1733 "principal" : {
1734 "type" : "User",
1735 "id" : "alice"
1736 },
1737 "action" : {
1738 "type" : "Photo",
1739 "id" : "view"
1740 },
1741 "resource" : {
1742 "type" : "Photo",
1743 "id" : "door"
1744 },
1745 "context" : {
1746 "is_authenticated": true,
1747 "is_authenticated": false
1748 },
1749 "policies" : {},
1750 "entities" : [],
1751 }"#;
1752 assert_is_authorized_json_str_is_failure(
1753 call,
1754 "the key `is_authenticated` occurs two or more times in the same JSON object at line 17 column 13",
1755 );
1756 }
1757
1758 #[test]
1759 fn test_request_validation() {
1760 let good_call = json!({
1761 "principal" : {
1762 "type": "User",
1763 "id": "alice",
1764 },
1765 "action": {
1766 "type": "Action",
1767 "id": "view",
1768 },
1769 "resource": {
1770 "type": "Photo",
1771 "id": "door",
1772 },
1773 "context": {},
1774 "policies": {
1775 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1776 },
1777 "entities": [],
1778 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };"
1779 });
1780 let bad_call = json!({
1781 "principal" : {
1782 "type": "User",
1783 "id": "alice",
1784 },
1785 "action": {
1786 "type": "Action",
1787 "id": "view",
1788 },
1789 "resource": {
1790 "type": "User",
1791 "id": "bob",
1792 },
1793 "context": {},
1794 "policies": {
1795 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1796 },
1797 "entities": [],
1798 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };"
1799 });
1800 let bad_call_req_validation_disabled = json!({
1801 "principal" : {
1802 "type": "User",
1803 "id": "alice",
1804 },
1805 "action": {
1806 "type": "Action",
1807 "id": "view",
1808 },
1809 "resource": {
1810 "type": "User",
1811 "id": "bob",
1812 },
1813 "context": {},
1814 "policies": {
1815 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1816 },
1817 "entities": [],
1818 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };",
1819 "validateRequest": false,
1820 });
1821
1822 assert_is_authorized_json(good_call);
1823 let errs = assert_is_authorized_json_is_failure(bad_call);
1824 assert_exactly_one_error(
1825 &errs,
1826 "resource type `User` is not valid for `Action::\"view\"`",
1827 Some("valid resource types for `Action::\"view\"`: `Photo`"),
1828 );
1829 assert_is_authorized_json(bad_call_req_validation_disabled);
1830 }
1831
1832 #[test]
1833 fn test_preparse_policy_set_success() {
1834 let policies = json!({
1835 "staticPolicies": "permit(principal == User::\"alice\", action, resource);"
1836 });
1837 let policy_set: PolicySet = serde_json::from_value(policies).unwrap();
1838
1839 let result = preparse_policy_set("test_policy_set".to_string(), policy_set);
1840 assert_matches!(result, CheckParseAnswer::Success);
1841 }
1842
1843 #[test]
1844 fn test_preparse_policy_set_failure() {
1845 let policies = json!({
1846 "staticPolicies": "invalid policy syntax"
1847 });
1848 let policy_set: PolicySet = serde_json::from_value(policies).unwrap();
1849
1850 let result = preparse_policy_set("test_policy_set".to_string(), policy_set);
1851 assert_matches!(result, CheckParseAnswer::Failure { .. });
1852 }
1853
1854 #[test]
1855 fn test_preparse_schema_success() {
1856 let schema =
1857 json!("entity User; action view appliesTo { principal: User, resource: User };");
1858 let schema_obj: Schema = serde_json::from_value(schema).unwrap();
1859
1860 let result = preparse_schema("test_schema".to_string(), schema_obj);
1861 assert_matches!(result, CheckParseAnswer::Success);
1862 }
1863
1864 #[test]
1865 fn test_preparse_schema_failure() {
1866 let schema = json!("invalid schema syntax");
1867 let schema_obj: Schema = serde_json::from_value(schema).unwrap();
1868
1869 let result = preparse_schema("test_schema".to_string(), schema_obj);
1870 assert_matches!(result, CheckParseAnswer::Failure { .. });
1871 }
1872
1873 #[test]
1874 fn test_stateful_is_authorized_success() {
1875 let policies = json!({
1877 "staticPolicies": "permit(principal == User::\"alice\", action, resource);"
1878 });
1879 let policy_set: PolicySet = serde_json::from_value(policies).unwrap();
1880 preparse_policy_set("test_policies".to_string(), policy_set);
1881
1882 let schema =
1883 json!("entity User; action view appliesTo { principal: User, resource: User };");
1884 let schema_obj: Schema = serde_json::from_value(schema).unwrap();
1885 preparse_schema("test_schema".to_string(), schema_obj);
1886
1887 let call = json!({
1889 "principal": {
1890 "type": "User",
1891 "id": "alice"
1892 },
1893 "action": {
1894 "type": "Action",
1895 "id": "view"
1896 },
1897 "resource": {
1898 "type": "User",
1899 "id": "bob"
1900 },
1901 "context": {},
1902 "preparsedSchemaName": "test_schema",
1903 "preparsedPolicySetId": "test_policies",
1904 "entities": []
1905 });
1906
1907 let stateful_call: StatefulAuthorizationCall = serde_json::from_value(call).unwrap();
1908 let result = stateful_is_authorized(stateful_call);
1909
1910 assert_matches!(result, AuthorizationAnswer::Success { response, .. } => {
1911 assert_eq!(response.decision(), Decision::Allow);
1912 });
1913 }
1914
1915 #[test]
1916 fn test_stateful_is_authorized_missing_policy_set() {
1917 let call = json!({
1918 "principal": {
1919 "type": "User",
1920 "id": "alice"
1921 },
1922 "action": {
1923 "type": "Action",
1924 "id": "view"
1925 },
1926 "resource": {
1927 "type": "User",
1928 "id": "bob"
1929 },
1930 "context": {},
1931 "preparsedPolicySetId": "nonexistent_policies",
1932 "entities": []
1933 });
1934
1935 let stateful_call: StatefulAuthorizationCall = serde_json::from_value(call).unwrap();
1936 let result = stateful_is_authorized(stateful_call);
1937
1938 assert_matches!(result, AuthorizationAnswer::Failure { errors, .. } => {
1939 assert!(!errors.is_empty());
1940 assert!(errors[0].message.contains("preparsed policy set 'nonexistent_policies' not found"));
1941 });
1942 }
1943
1944 #[test]
1945 fn test_stateful_is_authorized_without_schema() {
1946 let policies = json!({
1948 "staticPolicies": "permit(principal == User::\"alice\", action, resource);"
1949 });
1950 let policy_set: PolicySet = serde_json::from_value(policies).unwrap();
1951 preparse_policy_set("test_policies".to_string(), policy_set);
1952
1953 let call = json!({
1954 "principal": {
1955 "type": "User",
1956 "id": "alice"
1957 },
1958 "action": {
1959 "type": "Action",
1960 "id": "view"
1961 },
1962 "resource": {
1963 "type": "User",
1964 "id": "bob"
1965 },
1966 "context": {},
1967 "preparsedPolicySetId": "test_policies",
1968 "entities": []
1969 });
1970
1971 let stateful_call: StatefulAuthorizationCall = serde_json::from_value(call).unwrap();
1972 let result = stateful_is_authorized(stateful_call);
1973
1974 assert_matches!(result, AuthorizationAnswer::Success { response, .. } => {
1976 assert_eq!(response.decision(), Decision::Allow);
1977 });
1978 }
1979}
1980
1981#[cfg(feature = "partial-eval")]
1982#[cfg(test)]
1983mod partial_test {
1984 use super::*;
1985 use cool_asserts::assert_matches;
1986 use serde_json::json;
1987
1988 #[track_caller]
1989 fn assert_is_authorized_json_partial(call: serde_json::Value) {
1990 let ans_val = is_authorized_partial_json(call).unwrap();
1991 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
1992 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
1993 assert_eq!(response.decision(), Some(Decision::Allow));
1994 let errors: Vec<_> = response.errored().collect();
1995 assert_eq!(errors.len(), 0, "{errors:?}");
1996 });
1997 }
1998
1999 #[track_caller]
2000 fn assert_is_not_authorized_json_partial(call: serde_json::Value) {
2001 let ans_val = is_authorized_partial_json(call).unwrap();
2002 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
2003 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
2004 assert_eq!(response.decision(), Some(Decision::Deny));
2005 let errors: Vec<_> = response.errored().collect();
2006 assert_eq!(errors.len(), 0, "{errors:?}");
2007 });
2008 }
2009
2010 #[track_caller]
2011 fn assert_is_residual(call: serde_json::Value, expected_residuals: &HashSet<&str>) {
2012 let ans_val = is_authorized_partial_json(call).unwrap();
2013 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
2014 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
2015 assert_eq!(response.decision(), None);
2016 let errors: Vec<_> = response.errored().collect();
2017 assert_eq!(errors.len(), 0, "{errors:?}");
2018 let actual_residuals: HashSet<_> = response.nontrivial_residual_ids().collect();
2019 for id in expected_residuals {
2020 assert!(actual_residuals.contains(&PolicyId::new(id)), "expected nontrivial residual for {id}, but it's missing");
2021 }
2022 for id in &actual_residuals {
2023 assert!(expected_residuals.contains(id.to_string().as_str()),"found unexpected nontrivial residual for {id}");
2024 }
2025 });
2026 }
2027
2028 #[test]
2029 fn test_authorized_partial_no_resource() {
2030 let call = json!({
2031 "principal": {
2032 "type": "User",
2033 "id": "alice"
2034 },
2035 "action": {
2036 "type": "Photo",
2037 "id": "view"
2038 },
2039 "context": {},
2040 "policies": {
2041 "staticPolicies": {
2042 "ID1": "permit(principal == User::\"alice\", action, resource);"
2043 }
2044 },
2045 "entities": []
2046 });
2047
2048 assert_is_authorized_json_partial(call);
2049 }
2050
2051 #[test]
2052 fn test_authorized_partial_not_authorized_no_resource() {
2053 let call = json!({
2054 "principal": {
2055 "type": "User",
2056 "id": "john"
2057 },
2058 "action": {
2059 "type": "Photo",
2060 "id": "view"
2061 },
2062 "context": {},
2063 "policies": {
2064 "staticPolicies": {
2065 "ID1": "permit(principal == User::\"alice\", action, resource);"
2066 }
2067 },
2068 "entities": []
2069 });
2070
2071 assert_is_not_authorized_json_partial(call);
2072 }
2073
2074 #[test]
2075 fn test_authorized_partial_residual_no_principal_scope() {
2076 let call = json!({
2077 "action": {
2078 "type": "Photo",
2079 "id": "view"
2080 },
2081 "resource" : {
2082 "type" : "Photo",
2083 "id" : "door"
2084 },
2085 "context": {},
2086 "policies": {
2087 "staticPolicies": {
2088 "ID1": "permit(principal == User::\"alice\", action, resource);"
2089 }
2090 },
2091 "entities": []
2092 });
2093
2094 assert_is_residual(call, &HashSet::from(["ID1"]));
2095 }
2096
2097 #[test]
2098 fn test_authorized_partial_residual_no_principal_when() {
2099 let call = json!({
2100 "action": {
2101 "type": "Photo",
2102 "id": "view"
2103 },
2104 "resource" : {
2105 "type" : "Photo",
2106 "id" : "door"
2107 },
2108 "context": {},
2109 "policies" : {
2110 "staticPolicies" : {
2111 "ID1": "permit(principal, action, resource) when { principal == User::\"alice\" };"
2112 }
2113 },
2114 "entities": []
2115 });
2116
2117 assert_is_residual(call, &HashSet::from(["ID1"]));
2118 }
2119
2120 #[test]
2121 fn test_authorized_partial_residual_no_principal_ignored_forbid() {
2122 let call = json!({
2123 "action": {
2124 "type": "Photo",
2125 "id": "view"
2126 },
2127 "resource" : {
2128 "type" : "Photo",
2129 "id" : "door"
2130 },
2131 "context": {},
2132 "policies" : {
2133 "staticPolicies" : {
2134 "ID1": "permit(principal, action, resource) when { principal == User::\"alice\" };",
2135 "ID2": "forbid(principal, action, resource) unless { resource == Photo::\"door\" };"
2136 }
2137 },
2138 "entities": []
2139 });
2140
2141 assert_is_residual(call, &HashSet::from(["ID1"]));
2142 }
2143}