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