1#![allow(clippy::module_name_repetitions)]
21#[cfg(feature = "partial-eval")]
22use super::utils::JsonValueWithNoDuplicateKeys;
23use super::utils::{Context, DetailedError, Entities, EntityUid, PolicySet, Schema, WithWarnings};
24use crate::{Authorizer, Decision, PolicyId, Request};
25use cedar_policy_validator::cedar_schema::SchemaWarning;
26use serde::{Deserialize, Serialize};
27use serde_with::serde_as;
28#[cfg(feature = "partial-eval")]
29use std::collections::HashMap;
30use std::collections::HashSet;
31#[cfg(feature = "partial-eval")]
32use std::convert::Infallible;
33#[cfg(feature = "wasm")]
34use wasm_bindgen::prelude::wasm_bindgen;
35
36#[cfg(feature = "wasm")]
37extern crate tsify;
38
39thread_local!(
40 static AUTHORIZER: Authorizer = Authorizer::new();
42);
43
44#[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "isAuthorized"))]
46pub fn is_authorized(call: AuthorizationCall) -> AuthorizationAnswer {
47 match call.parse() {
48 WithWarnings {
49 t: Ok((request, policies, entities)),
50 warnings,
51 } => AuthorizationAnswer::Success {
52 response: AUTHORIZER.with(|authorizer| {
53 authorizer
54 .is_authorized(&request, &policies, &entities)
55 .into()
56 }),
57 warnings: warnings.into_iter().map(Into::into).collect(),
58 },
59 WithWarnings {
60 t: Err(errors),
61 warnings,
62 } => AuthorizationAnswer::Failure {
63 errors: errors.into_iter().map(Into::into).collect(),
64 warnings: warnings.into_iter().map(Into::into).collect(),
65 },
66 }
67}
68
69pub fn is_authorized_json(json: serde_json::Value) -> Result<serde_json::Value, serde_json::Error> {
77 let ans = is_authorized(serde_json::from_value(json)?);
78 serde_json::to_value(ans)
79}
80
81pub fn is_authorized_json_str(json: &str) -> Result<String, serde_json::Error> {
89 let ans = is_authorized(serde_json::from_str(json)?);
90 serde_json::to_string(&ans)
91}
92
93#[doc = include_str!("../../experimental_warning.md")]
96#[cfg(feature = "partial-eval")]
97pub fn is_authorized_partial(call: PartialAuthorizationCall) -> PartialAuthorizationAnswer {
98 match call.parse() {
99 WithWarnings {
100 t: Ok((request, policies, entities)),
101 warnings,
102 } => {
103 let response = AUTHORIZER.with(|authorizer| {
104 authorizer.is_authorized_partial(&request, &policies, &entities)
105 });
106 let warnings = warnings.into_iter().map(Into::into).collect();
107 match ResidualResponse::try_from(response) {
108 Ok(response) => PartialAuthorizationAnswer::Residuals {
109 response: Box::new(response),
110 warnings,
111 },
112 Err(e) => PartialAuthorizationAnswer::Failure {
113 errors: vec![miette::Report::new_boxed(e).into()],
114 warnings,
115 },
116 }
117 }
118 WithWarnings {
119 t: Err(errors),
120 warnings,
121 } => PartialAuthorizationAnswer::Failure {
122 errors: errors.into_iter().map(Into::into).collect(),
123 warnings: warnings.into_iter().map(Into::into).collect(),
124 },
125 }
126}
127
128#[doc = include_str!("../../experimental_warning.md")]
136#[cfg(feature = "partial-eval")]
137pub fn is_authorized_partial_json(
138 json: serde_json::Value,
139) -> Result<serde_json::Value, serde_json::Error> {
140 let ans = is_authorized_partial(serde_json::from_value(json)?);
141 serde_json::to_value(ans)
142}
143
144#[doc = include_str!("../../experimental_warning.md")]
152#[cfg(feature = "partial-eval")]
153pub fn is_authorized_partial_json_str(json: &str) -> Result<String, serde_json::Error> {
154 let ans = is_authorized_partial(serde_json::from_str(json)?);
155 serde_json::to_string(&ans)
156}
157
158#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
160#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
161#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
162#[serde(rename_all = "camelCase")]
163#[serde(deny_unknown_fields)]
164pub struct Response {
165 decision: Decision,
167 diagnostics: Diagnostics,
169}
170
171#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
174#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
175#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
176#[serde(rename_all = "camelCase")]
177#[serde(deny_unknown_fields)]
178pub struct Diagnostics {
179 reason: HashSet<PolicyId>,
182 errors: HashSet<AuthorizationError>,
184}
185
186impl Response {
187 pub fn new(
189 decision: Decision,
190 reason: HashSet<PolicyId>,
191 errors: HashSet<AuthorizationError>,
192 ) -> Self {
193 Self {
194 decision,
195 diagnostics: Diagnostics { reason, errors },
196 }
197 }
198
199 pub fn decision(&self) -> Decision {
201 self.decision
202 }
203
204 pub fn diagnostics(&self) -> &Diagnostics {
206 &self.diagnostics
207 }
208}
209
210impl From<crate::Response> for Response {
211 fn from(response: crate::Response) -> Self {
212 let (reason, errors) = response.diagnostics.into_components();
213 Self::new(
214 response.decision,
215 reason.collect(),
216 errors.map(Into::into).collect(),
217 )
218 }
219}
220
221#[cfg(feature = "partial-eval")]
222impl TryFrom<crate::PartialResponse> for Response {
223 type Error = Infallible;
224
225 fn try_from(partial_response: crate::PartialResponse) -> Result<Self, Self::Error> {
226 Ok(partial_response.concretize().into())
227 }
228}
229
230impl Diagnostics {
231 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
233 self.reason.iter()
234 }
235
236 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
238 self.errors.iter()
239 }
240}
241
242#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
244#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
245#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
246#[serde(rename_all = "camelCase")]
247#[serde(deny_unknown_fields)]
248pub struct AuthorizationError {
249 #[cfg_attr(feature = "wasm", tsify(type = "string"))]
251 pub policy_id: PolicyId,
252 pub error: DetailedError,
256}
257
258impl AuthorizationError {
259 pub fn new(
261 policy_id: impl Into<PolicyId>,
262 error: impl miette::Diagnostic + Send + Sync + 'static,
263 ) -> Self {
264 Self::new_from_report(policy_id, miette::Report::new(error))
265 }
266
267 pub fn new_from_report(policy_id: impl Into<PolicyId>, report: miette::Report) -> Self {
269 Self {
270 policy_id: policy_id.into(),
271 error: report.into(),
272 }
273 }
274}
275
276impl From<crate::AuthorizationError> for AuthorizationError {
277 fn from(e: crate::AuthorizationError) -> Self {
278 match e {
279 crate::AuthorizationError::PolicyEvaluationError(e) => {
280 Self::new(e.policy_id().clone(), e.into_inner())
281 }
282 }
283 }
284}
285
286#[doc(hidden)]
287impl From<cedar_policy_core::authorizer::AuthorizationError> for AuthorizationError {
288 fn from(e: cedar_policy_core::authorizer::AuthorizationError) -> Self {
289 crate::AuthorizationError::from(e).into()
290 }
291}
292
293#[doc = include_str!("../../experimental_warning.md")]
295#[cfg(feature = "partial-eval")]
296#[derive(Debug, Clone, Serialize, Deserialize)]
297#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
298#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
299#[serde(rename_all = "camelCase")]
300#[serde(deny_unknown_fields)]
301pub struct ResidualResponse {
302 decision: Option<Decision>,
303 satisfied: HashSet<PolicyId>,
304 errored: HashSet<PolicyId>,
305 may_be_determining: HashSet<PolicyId>,
306 must_be_determining: HashSet<PolicyId>,
307 residuals: HashMap<PolicyId, JsonValueWithNoDuplicateKeys>,
308 nontrivial_residuals: HashSet<PolicyId>,
309}
310
311#[cfg(feature = "partial-eval")]
312impl ResidualResponse {
313 pub fn decision(&self) -> Option<Decision> {
315 self.decision
316 }
317
318 pub fn satisfied(&self) -> impl Iterator<Item = &PolicyId> {
320 self.satisfied.iter()
321 }
322
323 pub fn errored(&self) -> impl Iterator<Item = &PolicyId> {
325 self.errored.iter()
326 }
327
328 pub fn may_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
330 self.may_be_determining.iter()
331 }
332
333 pub fn must_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
335 self.must_be_determining.iter()
336 }
337
338 pub fn residuals(&self) -> impl Iterator<Item = &JsonValueWithNoDuplicateKeys> {
340 self.residuals.values()
341 }
342
343 pub fn into_residuals(self) -> impl Iterator<Item = JsonValueWithNoDuplicateKeys> {
345 self.residuals.into_values()
346 }
347
348 pub fn residual(&self, p: &PolicyId) -> Option<&JsonValueWithNoDuplicateKeys> {
350 self.residuals.get(p)
351 }
352
353 pub fn nontrivial_residuals(&self) -> impl Iterator<Item = &JsonValueWithNoDuplicateKeys> {
355 self.residuals.iter().filter_map(|(id, policy)| {
356 if self.nontrivial_residuals.contains(id) {
357 Some(policy)
358 } else {
359 None
360 }
361 })
362 }
363
364 pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &PolicyId> {
366 self.nontrivial_residuals.iter()
367 }
368}
369
370#[cfg(feature = "partial-eval")]
371impl TryFrom<crate::PartialResponse> for ResidualResponse {
372 type Error = Box<dyn miette::Diagnostic + Send + Sync + 'static>;
373
374 fn try_from(partial_response: crate::PartialResponse) -> Result<Self, Self::Error> {
375 Ok(Self {
376 decision: partial_response.decision(),
377 satisfied: partial_response
378 .definitely_satisfied()
379 .map(|p| p.id().clone())
380 .collect(),
381 errored: partial_response.definitely_errored().cloned().collect(),
382 may_be_determining: partial_response
383 .may_be_determining()
384 .map(|p| p.id().clone())
385 .collect(),
386 must_be_determining: partial_response
387 .must_be_determining()
388 .map(|p| p.id().clone())
389 .collect(),
390 nontrivial_residuals: partial_response
391 .nontrivial_residuals()
392 .map(|p| p.id().clone())
393 .collect(),
394 residuals: partial_response
395 .all_residuals()
396 .map(|e| e.to_json().map(|json| (e.id().clone(), json.into())))
397 .collect::<Result<_, _>>()?,
398 })
399 }
400}
401
402#[derive(Debug, Serialize, Deserialize)]
404#[serde(tag = "type")]
405#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
406#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
407#[serde(rename_all = "camelCase")]
408pub enum AuthorizationAnswer {
409 #[serde(rename_all = "camelCase")]
411 Failure {
412 errors: Vec<DetailedError>,
414 warnings: Vec<DetailedError>,
416 },
417 #[serde(rename_all = "camelCase")]
420 Success {
421 response: Response,
424 warnings: Vec<DetailedError>,
429 },
430}
431
432#[cfg(feature = "partial-eval")]
434#[derive(Debug, Serialize, Deserialize)]
435#[serde(tag = "type")]
436#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
437#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
438#[serde(rename_all = "camelCase")]
439pub enum PartialAuthorizationAnswer {
440 #[serde(rename_all = "camelCase")]
442 Failure {
443 errors: Vec<DetailedError>,
445 warnings: Vec<DetailedError>,
447 },
448 #[serde(rename_all = "camelCase")]
451 Residuals {
452 response: Box<ResidualResponse>,
454 warnings: Vec<DetailedError>,
459 },
460}
461
462#[serde_as]
464#[derive(Debug, Serialize, Deserialize)]
465#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
466#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
467#[serde(rename_all = "camelCase")]
468#[serde(deny_unknown_fields)]
469pub struct AuthorizationCall {
470 principal: EntityUid,
472 action: EntityUid,
474 resource: EntityUid,
476 context: Context,
478 #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
483 schema: Option<Schema>,
484 #[serde(default = "constant_true")]
489 validate_request: bool,
490 policies: PolicySet,
492 entities: Entities,
494}
495
496#[cfg(feature = "partial-eval")]
498#[serde_as]
499#[derive(Debug, Serialize, Deserialize)]
500#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
501#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
502#[serde(rename_all = "camelCase")]
503#[serde(deny_unknown_fields)]
504pub struct PartialAuthorizationCall {
505 principal: Option<EntityUid>,
507 action: Option<EntityUid>,
509 resource: Option<EntityUid>,
511 context: Context,
513 #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
518 schema: Option<Schema>,
519 #[serde(default = "constant_true")]
524 validate_request: bool,
525 policies: PolicySet,
527 entities: Entities,
529}
530
531fn constant_true() -> bool {
532 true
533}
534
535fn build_error<T>(
536 errs: Vec<miette::Report>,
537 warnings: Vec<SchemaWarning>,
538) -> WithWarnings<Result<T, Vec<miette::Report>>> {
539 WithWarnings {
540 t: Err(errs),
541 warnings: warnings.into_iter().map(Into::into).collect(),
542 }
543}
544
545impl AuthorizationCall {
546 fn parse(
547 self,
548 ) -> WithWarnings<Result<(Request, crate::PolicySet, crate::Entities), Vec<miette::Report>>>
549 {
550 let mut errs = vec![];
551 let mut warnings = vec![];
552 let maybe_schema = self
553 .schema
554 .map(|schema| {
555 schema.parse().map(|(schema, new_warnings)| {
556 warnings.extend(new_warnings);
557 schema
558 })
559 })
560 .transpose()
561 .map_err(|e| errs.push(e));
562 let maybe_principal = self
563 .principal
564 .parse(Some("principal"))
565 .map_err(|e| errs.push(e));
566 let maybe_action = self.action.parse(Some("action")).map_err(|e| errs.push(e));
567 let maybe_resource = self
568 .resource
569 .parse(Some("resource"))
570 .map_err(|e| errs.push(e));
571
572 let (Ok(schema), Ok(principal), Ok(action), Ok(resource)) =
573 (maybe_schema, maybe_principal, maybe_action, maybe_resource)
574 else {
575 return build_error(errs, warnings);
577 };
578
579 let context = match self.context.parse(schema.as_ref(), Some(&action)) {
580 Ok(context) => context,
581 Err(e) => {
582 return build_error(vec![e], warnings);
583 }
584 };
585
586 let schema_opt = if self.validate_request {
587 schema.as_ref()
588 } else {
589 None
590 };
591 let maybe_request = Request::new(principal, action, resource, context, schema_opt)
592 .map_err(|e| errs.push(e.into()));
593 let maybe_entities = self
594 .entities
595 .parse(schema.as_ref())
596 .map_err(|e| errs.push(e));
597 let maybe_policies = self.policies.parse().map_err(|es| errs.extend(es));
598
599 match (maybe_request, maybe_policies, maybe_entities) {
600 (Ok(request), Ok(policies), Ok(entities)) => WithWarnings {
601 t: Ok((request, policies, entities)),
602 warnings: warnings.into_iter().map(Into::into).collect(),
603 },
604 _ => {
605 build_error(errs, warnings)
607 }
608 }
609 }
610}
611
612#[cfg(feature = "partial-eval")]
613impl PartialAuthorizationCall {
614 fn parse(
615 self,
616 ) -> WithWarnings<Result<(Request, crate::PolicySet, crate::Entities), Vec<miette::Report>>>
617 {
618 let mut errs = vec![];
619 let mut warnings = vec![];
620 let maybe_schema = self
621 .schema
622 .map(|schema| {
623 schema.parse().map(|(schema, new_warnings)| {
624 warnings.extend(new_warnings);
625 schema
626 })
627 })
628 .transpose()
629 .map_err(|e| errs.push(e));
630 let maybe_principal = self
631 .principal
632 .map(|uid| uid.parse(Some("principal")))
633 .transpose()
634 .map_err(|e| errs.push(e));
635 let maybe_action = self
636 .action
637 .map(|uid| uid.parse(Some("action")))
638 .transpose()
639 .map_err(|e| errs.push(e));
640 let maybe_resource = self
641 .resource
642 .map(|uid| uid.parse(Some("resource")))
643 .transpose()
644 .map_err(|e| errs.push(e));
645
646 let (Ok(schema), Ok(principal), Ok(action), Ok(resource)) =
647 (maybe_schema, maybe_principal, maybe_action, maybe_resource)
648 else {
649 return build_error(errs, warnings);
651 };
652
653 let context = match self.context.parse(schema.as_ref(), action.as_ref()) {
654 Ok(context) => context,
655 Err(e) => {
656 return build_error(vec![e], warnings);
657 }
658 };
659
660 let maybe_entities = self
661 .entities
662 .parse(schema.as_ref())
663 .map_err(|e| errs.push(e));
664 let maybe_policies = self.policies.parse().map_err(|es| errs.extend(es));
665
666 let mut b = Request::builder();
667 if let Some(p) = principal {
668 b = b.principal(p);
669 }
670 if let Some(a) = action {
671 b = b.action(a);
672 }
673 if let Some(r) = resource {
674 b = b.resource(r);
675 }
676 b = b.context(context);
677
678 let maybe_request = match schema {
679 Some(schema) if self.validate_request => {
680 b.schema(&schema).build().map_err(|e| errs.push(e.into()))
681 }
682 _ => Ok(b.build()),
683 };
684
685 match (maybe_request, maybe_policies, maybe_entities) {
686 (Ok(request), Ok(policies), Ok(entities)) => WithWarnings {
687 t: Ok((request, policies, entities)),
688 warnings: warnings.into_iter().map(Into::into).collect(),
689 },
690 _ => {
691 build_error(errs, warnings)
693 }
694 }
695 }
696}
697
698#[allow(clippy::panic)]
700#[cfg(test)]
701mod test {
702 use super::*;
703
704 use crate::ffi::test_utils::*;
705 use cool_asserts::assert_matches;
706 use serde_json::json;
707
708 #[track_caller]
710 fn assert_is_authorized_json(json: serde_json::Value) {
711 let ans_val =
712 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
713 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
714 assert_matches!(result, Ok(AuthorizationAnswer::Success { response, .. }) => {
715 assert_eq!(response.decision(), Decision::Allow);
716 let errors: Vec<&AuthorizationError> = response.diagnostics().errors().collect();
717 assert_eq!(errors.len(), 0, "{errors:?}");
718 });
719 }
720
721 #[track_caller]
723 fn assert_is_not_authorized_json(json: serde_json::Value) {
724 let ans_val =
725 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
726 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
727 assert_matches!(result, Ok(AuthorizationAnswer::Success { response, .. }) => {
728 assert_eq!(response.decision(), Decision::Deny);
729 let errors: Vec<&AuthorizationError> = response.diagnostics().errors().collect();
730 assert_eq!(errors.len(), 0, "{errors:?}");
731 });
732 }
733
734 #[track_caller]
737 fn assert_is_authorized_json_str_is_failure(call: &str, msg: &str) {
738 assert_matches!(is_authorized_json_str(call), Err(e) => {
739 assert_eq!(e.to_string(), msg);
740 });
741 }
742
743 #[track_caller]
746 fn assert_is_authorized_json_is_failure(json: serde_json::Value) -> Vec<DetailedError> {
747 let ans_val =
748 is_authorized_json(json).expect("expected input to parse as an `AuthorizationCall`");
749 let result: Result<AuthorizationAnswer, _> = serde_json::from_value(ans_val);
750 assert_matches!(result, Ok(AuthorizationAnswer::Failure { errors, .. }) => errors)
751 }
752
753 #[test]
754 fn test_failure_on_invalid_syntax() {
755 assert_is_authorized_json_str_is_failure(
756 "iefjieoafiaeosij",
757 "expected value at line 1 column 1",
758 );
759 }
760
761 #[test]
762 fn test_not_authorized_on_empty_slice() {
763 let call = json!({
764 "principal": {
765 "type": "User",
766 "id": "alice"
767 },
768 "action": {
769 "type": "Photo",
770 "id": "view"
771 },
772 "resource": {
773 "type": "Photo",
774 "id": "door"
775 },
776 "context": {},
777 "policies": {},
778 "entities": []
779 });
780 assert_is_not_authorized_json(call);
781 }
782
783 #[test]
784 fn test_not_authorized_on_unspecified() {
785 let call = json!({
786 "principal": null,
787 "action": {
788 "type": "Photo",
789 "id": "view"
790 },
791 "resource": {
792 "type": "Photo",
793 "id": "door"
794 },
795 "context": {},
796 "policies": {
797 "staticPolicies": {
798 "ID1": "permit(principal == User::\"alice\", action, resource);"
799 }
800 },
801 "entities": []
802 });
803 let errs = assert_is_authorized_json_is_failure(call);
805 assert_exactly_one_error(
806 &errs,
807 "failed to parse principal: in uid field of <unknown entity>, expected a literal entity reference, but got `null`",
808 Some("literal entity references can be made with `{ \"type\": \"SomeType\", \"id\": \"SomeId\" }`"),
809 );
810 }
811
812 #[test]
813 fn test_authorized_on_simple_slice() {
814 let call = json!({
815 "principal": {
816 "type": "User",
817 "id": "alice"
818 },
819 "action": {
820 "type": "Photo",
821 "id": "view"
822 },
823 "resource": {
824 "type": "Photo",
825 "id": "door"
826 },
827 "context": {},
828 "policies": {
829 "staticPolicies": {
830 "ID1": "permit(principal == User::\"alice\", action, resource);"
831 }
832 },
833 "entities": []
834 });
835 assert_is_authorized_json(call);
836 }
837
838 #[test]
839 fn test_authorized_on_simple_slice_with_string_policies() {
840 let call = json!({
841 "principal": {
842 "type": "User",
843 "id": "alice"
844 },
845 "action": {
846 "type": "Photo",
847 "id": "view"
848 },
849 "resource": {
850 "type": "Photo",
851 "id": "door"
852 },
853 "context": {},
854 "policies": {
855 "staticPolicies": "permit(principal == User::\"alice\", action, resource);"
856 },
857 "entities": []
858 });
859 assert_is_authorized_json(call);
860 }
861
862 #[test]
863 fn test_authorized_on_simple_slice_with_context() {
864 let call = json!({
865 "principal": {
866 "type": "User",
867 "id": "alice"
868 },
869 "action": {
870 "type": "Photo",
871 "id": "view"
872 },
873 "resource": {
874 "type": "Photo",
875 "id": "door"
876 },
877 "context": {
878 "is_authenticated": true,
879 "source_ip": {
880 "__extn" : { "fn" : "ip", "arg" : "222.222.222.222" }
881 }
882 },
883 "policies": {
884 "staticPolicies": "permit(principal == User::\"alice\", action, resource) when { context.is_authenticated && context.source_ip.isInRange(ip(\"222.222.222.0/24\")) };"
885 },
886 "entities": []
887 });
888 assert_is_authorized_json(call);
889 }
890
891 #[test]
892 fn test_authorized_on_simple_slice_with_attrs_and_parents() {
893 let call = json!({
894 "principal": {
895 "type": "User",
896 "id": "alice"
897 },
898 "action": {
899 "type": "Photo",
900 "id": "view"
901 },
902 "resource": {
903 "type": "Photo",
904 "id": "door"
905 },
906 "context": {},
907 "policies": {
908 "staticPolicies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };"
909 },
910 "entities": [
911 {
912 "uid": {
913 "__entity": {
914 "type": "User",
915 "id": "alice"
916 }
917 },
918 "attrs": {},
919 "parents": []
920 },
921 {
922 "uid": {
923 "__entity": {
924 "type": "Photo",
925 "id": "door"
926 }
927 },
928 "attrs": {
929 "owner": {
930 "__entity": {
931 "type": "User",
932 "id": "alice"
933 }
934 }
935 },
936 "parents": [
937 {
938 "__entity": {
939 "type": "Folder",
940 "id": "house"
941 }
942 }
943 ]
944 },
945 {
946 "uid": {
947 "__entity": {
948 "type": "Folder",
949 "id": "house"
950 }
951 },
952 "attrs": {},
953 "parents": []
954 }
955 ]
956 });
957 assert_is_authorized_json(call);
958 }
959
960 #[test]
961 fn test_authorized_on_multi_policy_slice() {
962 let call = json!({
963 "principal": {
964 "type": "User",
965 "id": "alice"
966 },
967 "action": {
968 "type": "Photo",
969 "id": "view"
970 },
971 "resource": {
972 "type": "Photo",
973 "id": "door"
974 },
975 "context": {},
976 "policies": {
977 "staticPolicies": {
978 "ID0": "permit(principal == User::\"jerry\", action, resource == Photo::\"doorx\");",
979 "ID1": "permit(principal == User::\"tom\", action, resource == Photo::\"doory\");",
980 "ID2": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");"
981 }
982 },
983 "entities": []
984 });
985 assert_is_authorized_json(call);
986 }
987
988 #[test]
989 fn test_authorized_on_multi_policy_slice_with_string_policies() {
990 let call = json!({
991 "principal": {
992 "type": "User",
993 "id": "alice"
994 },
995 "action": {
996 "type": "Photo",
997 "id": "view"
998 },
999 "resource": {
1000 "type": "Photo",
1001 "id": "door"
1002 },
1003 "context": {},
1004 "policies": {
1005 "staticPolicies": "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };"
1006 },
1007 "entities": [
1008 {
1009 "uid": {
1010 "__entity": {
1011 "type": "User",
1012 "id": "alice"
1013 }
1014 },
1015 "attrs": {},
1016 "parents": []
1017 },
1018 {
1019 "uid": {
1020 "__entity": {
1021 "type": "Photo",
1022 "id": "door"
1023 }
1024 },
1025 "attrs": {
1026 "owner": {
1027 "__entity": {
1028 "type": "User",
1029 "id": "alice"
1030 }
1031 }
1032 },
1033 "parents": [
1034 {
1035 "__entity": {
1036 "type": "Folder",
1037 "id": "house"
1038 }
1039 }
1040 ]
1041 },
1042 {
1043 "uid": {
1044 "__entity": {
1045 "type": "Folder",
1046 "id": "house"
1047 }
1048 },
1049 "attrs": {},
1050 "parents": []
1051 }
1052 ]
1053 });
1054 assert_is_authorized_json(call);
1055 }
1056
1057 #[test]
1058 fn test_authorized_on_multi_policy_slice_denies_when_expected() {
1059 let call = json!({
1060 "principal": {
1061 "type": "User",
1062 "id": "alice"
1063 },
1064 "action": {
1065 "type": "Photo",
1066 "id": "view"
1067 },
1068 "resource": {
1069 "type": "Photo",
1070 "id": "door"
1071 },
1072 "context": {},
1073 "policies": {
1074 "staticPolicies": {
1075 "ID0": "permit(principal, action, resource);",
1076 "ID1": "forbid(principal == User::\"alice\", action, resource == Photo::\"door\");"
1077 }
1078 },
1079 "entities": []
1080 });
1081 assert_is_not_authorized_json(call);
1082 }
1083
1084 #[test]
1085 fn test_authorized_on_multi_policy_slice_with_string_policies_denies_when_expected() {
1086 let call = json!({
1087 "principal": {
1088 "type": "User",
1089 "id": "alice"
1090 },
1091 "action": {
1092 "type": "Photo",
1093 "id": "view"
1094 },
1095 "resource": {
1096 "type": "Photo",
1097 "id": "door"
1098 },
1099 "context": {},
1100 "policies": {
1101 "staticPolicies": "permit(principal, action, resource);\nforbid(principal == User::\"alice\", action, resource);"
1102 },
1103 "entities": []
1104 });
1105 assert_is_not_authorized_json(call);
1106 }
1107
1108 #[test]
1109 fn test_authorized_with_template_as_policy_should_fail() {
1110 let call = json!({
1111 "principal": {
1112 "type": "User",
1113 "id": "alice"
1114 },
1115 "action": {
1116 "type": "Photo",
1117 "id": "view"
1118 },
1119 "resource": {
1120 "type": "Photo",
1121 "id": "door"
1122 },
1123 "context": {},
1124 "policies": {
1125 "staticPolicies": "permit(principal == ?principal, action, resource);"
1126 },
1127 "entities": []
1128 });
1129 let errs = assert_is_authorized_json_is_failure(call);
1130 assert_exactly_one_error(&errs, "static policy set includes a template", None);
1131 }
1132
1133 #[test]
1134 fn test_authorized_with_template_should_fail() {
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 "templates": {
1151 "ID0": "permit(principal == ?principal, action, resource);"
1152 }
1153 },
1154 "entities": [],
1155 });
1156 assert_is_not_authorized_json(call);
1157 }
1158
1159 #[test]
1160 fn test_authorized_with_template_link() {
1161 let call = json!({
1162 "principal": {
1163 "type": "User",
1164 "id": "alice"
1165 },
1166 "action": {
1167 "type": "Photo",
1168 "id": "view"
1169 },
1170 "resource": {
1171 "type": "Photo",
1172 "id": "door"
1173 },
1174 "context": {},
1175 "policies": {
1176 "templates": {
1177 "ID0": "permit(principal == ?principal, action, resource);"
1178 },
1179 "templateLinks": [
1180 {
1181 "templateId": "ID0",
1182 "newId": "ID0_User_alice",
1183 "values": {
1184 "?principal": { "type": "User", "id": "alice" }
1185 }
1186 }
1187 ]
1188 },
1189 "entities": []
1190 });
1191 assert_is_authorized_json(call);
1192 }
1193
1194 #[test]
1195 fn test_authorized_fails_on_policy_collision_with_template() {
1196 let call = json!({
1197 "principal" : {
1198 "type" : "User",
1199 "id" : "alice"
1200 },
1201 "action" : {
1202 "type" : "Action",
1203 "id" : "view"
1204 },
1205 "resource" : {
1206 "type" : "Photo",
1207 "id" : "door"
1208 },
1209 "context" : {},
1210 "policies": {
1211 "staticPolicies": {
1212 "ID0": "permit(principal, action, resource);"
1213 },
1214 "templates": {
1215 "ID0": "permit(principal == ?principal, action, resource);"
1216 }
1217 },
1218 "entities" : []
1219 });
1220 let errs = assert_is_authorized_json_is_failure(call);
1221 assert_exactly_one_error(
1222 &errs,
1223 "failed to add template with id `ID0` to policy set: duplicate template or policy id `ID0`",
1224 None,
1225 );
1226 }
1227
1228 #[test]
1229 fn test_authorized_fails_on_duplicate_link_ids() {
1230 let call = json!({
1231 "principal" : {
1232 "type" : "User",
1233 "id" : "alice"
1234 },
1235 "action" : {
1236 "type" : "Action",
1237 "id" : "view"
1238 },
1239 "resource" : {
1240 "type" : "Photo",
1241 "id" : "door"
1242 },
1243 "context" : {},
1244 "policies" : {
1245 "templates": {
1246 "ID0": "permit(principal == ?principal, action, resource);"
1247 },
1248 "templateLinks" : [
1249 {
1250 "templateId" : "ID0",
1251 "newId" : "ID1",
1252 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1253 },
1254 {
1255 "templateId" : "ID0",
1256 "newId" : "ID1",
1257 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1258 }
1259 ]
1260 },
1261 "entities" : [],
1262 });
1263 let errs = assert_is_authorized_json_is_failure(call);
1264 assert_exactly_one_error(
1265 &errs,
1266 "unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1267 None,
1268 );
1269 }
1270
1271 #[test]
1272 fn test_authorized_fails_on_template_link_collision_with_template() {
1273 let call = json!({
1274 "principal" : {
1275 "type" : "User",
1276 "id" : "alice"
1277 },
1278 "action" : {
1279 "type" : "Action",
1280 "id" : "view"
1281 },
1282 "resource" : {
1283 "type" : "Photo",
1284 "id" : "door"
1285 },
1286 "context" : {},
1287 "policies" : {
1288 "templates": {
1289 "ID0": "permit(principal == ?principal, action, resource);"
1290 },
1291 "templateLinks" : [
1292 {
1293 "templateId" : "ID0",
1294 "newId" : "ID0",
1295 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1296 }
1297 ]
1298 },
1299 "entities" : []
1300
1301 });
1302 let errs = assert_is_authorized_json_is_failure(call);
1303 assert_exactly_one_error(
1304 &errs,
1305 "unable to link template: template-linked policy id `ID0` conflicts with an existing policy id",
1306 None,
1307 );
1308 }
1309
1310 #[test]
1311 fn test_authorized_fails_on_template_link_collision_with_policy() {
1312 let call = json!({
1313 "principal" : {
1314 "type" : "User",
1315 "id" : "alice"
1316 },
1317 "action" : {
1318 "type" : "Action",
1319 "id" : "view"
1320 },
1321 "resource" : {
1322 "type" : "Photo",
1323 "id" : "door"
1324 },
1325 "context" : {},
1326 "policies" : {
1327 "staticPolicies" : {
1328 "ID1": "permit(principal, action, resource);"
1329 },
1330 "templates": {
1331 "ID0": "permit(principal == ?principal, action, resource);"
1332 },
1333 "templateLinks" : [
1334 {
1335 "templateId" : "ID0",
1336 "newId" : "ID1",
1337 "values" : { "?principal": { "type" : "User", "id" : "alice" } }
1338 }
1339 ]
1340 },
1341 "entities" : []
1342 });
1343 let errs = assert_is_authorized_json_is_failure(call);
1344 assert_exactly_one_error(
1345 &errs,
1346 "unable to link template: template-linked policy id `ID1` conflicts with an existing policy id",
1347 None,
1348 );
1349 }
1350
1351 #[test]
1352 fn test_authorized_fails_on_duplicate_policy_ids() {
1353 let call = r#"{
1354 "principal" : {
1355 "type" : "User",
1356 "id" : "alice"
1357 },
1358 "action" : {
1359 "type" : "Action",
1360 "id" : "view"
1361 },
1362 "resource" : {
1363 "type" : "Photo",
1364 "id" : "door"
1365 },
1366 "context" : {},
1367 "policies" : {
1368 "staticPolicies" : {
1369 "ID0": "permit(principal, action, resource);",
1370 "ID0": "permit(principal, action, resource);"
1371 }
1372 },
1373 "entities" : [],
1374 }"#;
1375 assert_is_authorized_json_str_is_failure(
1376 call,
1377 "expected a static policy set represented by a string, JSON array, or JSON object (with no duplicate keys) at line 20 column 13",
1378 );
1379 }
1380
1381 #[test]
1382 fn test_authorized_fails_on_duplicate_template_ids() {
1383 let call = r#"{
1384 "principal" : {
1385 "type" : "User",
1386 "id" : "alice"
1387 },
1388 "action" : {
1389 "type" : "Action",
1390 "id" : "view"
1391 },
1392 "resource" : {
1393 "type" : "Photo",
1394 "id" : "door"
1395 },
1396 "context" : {},
1397 "policies" : {
1398 "templates" : {
1399 "ID0": "permit(principal == ?principal, action, resource);",
1400 "ID0": "permit(principal == ?principal, action, resource);"
1401 }
1402 },
1403 "entities" : []
1404 }"#;
1405 assert_is_authorized_json_str_is_failure(
1406 call,
1407 "invalid entry: found duplicate key at line 19 column 17",
1408 );
1409 }
1410
1411 #[test]
1412 fn test_authorized_fails_on_duplicate_slot_link() {
1413 let call = r#"{
1414 "principal" : {
1415 "type" : "User",
1416 "id" : "alice"
1417 },
1418 "action" : {
1419 "type" : "Action",
1420 "id" : "view"
1421 },
1422 "resource" : {
1423 "type" : "Photo",
1424 "id" : "door"
1425 },
1426 "context" : {},
1427 "policies" : {
1428 "templates" : {
1429 "ID0": "permit(principal == ?principal, action, resource);"
1430 },
1431 "templateLinks" : [{
1432 "templateId" : "ID0",
1433 "newId" : "ID1",
1434 "values" : {
1435 "?principal": { "type" : "User", "id" : "alice" },
1436 "?principal": { "type" : "User", "id" : "alice" }
1437 }
1438 }]
1439 },
1440 "entities" : [],
1441 }"#;
1442 assert_is_authorized_json_str_is_failure(
1443 call,
1444 "invalid entry: found duplicate key at line 25 column 21",
1445 );
1446 }
1447
1448 #[test]
1449 fn test_authorized_fails_duplicate_entity_uid() {
1450 let call = json!({
1451 "principal" : {
1452 "type" : "User",
1453 "id" : "alice"
1454 },
1455 "action" : {
1456 "type" : "Photo",
1457 "id" : "view"
1458 },
1459 "resource" : {
1460 "type" : "Photo",
1461 "id" : "door"
1462 },
1463 "context" : {},
1464 "policies" : {},
1465 "entities" : [
1466 {
1467 "uid": {
1468 "type" : "User",
1469 "id" : "alice"
1470 },
1471 "attrs": {},
1472 "parents": []
1473 },
1474 {
1475 "uid": {
1476 "type" : "User",
1477 "id" : "alice"
1478 },
1479 "attrs": {},
1480 "parents": []
1481 }
1482 ]
1483 });
1484 let errs = assert_is_authorized_json_is_failure(call);
1485 assert_exactly_one_error(&errs, r#"duplicate entity entry `User::"alice"`"#, None);
1486 }
1487
1488 #[test]
1489 fn test_authorized_fails_duplicate_context_key() {
1490 let call = r#"{
1491 "principal" : {
1492 "type" : "User",
1493 "id" : "alice"
1494 },
1495 "action" : {
1496 "type" : "Photo",
1497 "id" : "view"
1498 },
1499 "resource" : {
1500 "type" : "Photo",
1501 "id" : "door"
1502 },
1503 "context" : {
1504 "is_authenticated": true,
1505 "is_authenticated": false
1506 },
1507 "policies" : {},
1508 "entities" : [],
1509 }"#;
1510 assert_is_authorized_json_str_is_failure(
1511 call,
1512 "the key `is_authenticated` occurs two or more times in the same JSON object at line 17 column 13",
1513 );
1514 }
1515
1516 #[test]
1517 fn test_request_validation() {
1518 let good_call = json!({
1519 "principal" : {
1520 "type": "User",
1521 "id": "alice",
1522 },
1523 "action": {
1524 "type": "Action",
1525 "id": "view",
1526 },
1527 "resource": {
1528 "type": "Photo",
1529 "id": "door",
1530 },
1531 "context": {},
1532 "policies": {
1533 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1534 },
1535 "entities": [],
1536 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };"
1537 });
1538 let bad_call = json!({
1539 "principal" : {
1540 "type": "User",
1541 "id": "alice",
1542 },
1543 "action": {
1544 "type": "Action",
1545 "id": "view",
1546 },
1547 "resource": {
1548 "type": "User",
1549 "id": "bob",
1550 },
1551 "context": {},
1552 "policies": {
1553 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1554 },
1555 "entities": [],
1556 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };"
1557 });
1558 let bad_call_req_validation_disabled = json!({
1559 "principal" : {
1560 "type": "User",
1561 "id": "alice",
1562 },
1563 "action": {
1564 "type": "Action",
1565 "id": "view",
1566 },
1567 "resource": {
1568 "type": "User",
1569 "id": "bob",
1570 },
1571 "context": {},
1572 "policies": {
1573 "staticPolicies": "permit(principal == User::\"alice\", action == Action::\"view\", resource);"
1574 },
1575 "entities": [],
1576 "schema": "entity User, Photo; action view appliesTo { principal: User, resource: Photo };",
1577 "validateRequest": false,
1578 });
1579
1580 assert_is_authorized_json(good_call);
1581 let errs = assert_is_authorized_json_is_failure(bad_call);
1582 assert_exactly_one_error(
1583 &errs,
1584 "resource type `User` is not valid for `Action::\"view\"`",
1585 Some("valid resource types for `Action::\"view\"`: `Photo`"),
1586 );
1587 assert_is_authorized_json(bad_call_req_validation_disabled);
1588 }
1589}
1590
1591#[cfg(feature = "partial-eval")]
1592#[cfg(test)]
1593mod partial_test {
1594 use super::*;
1595 use cool_asserts::assert_matches;
1596 use serde_json::json;
1597
1598 #[track_caller]
1599 fn assert_is_authorized_json_partial(call: serde_json::Value) {
1600 let ans_val = is_authorized_partial_json(call).unwrap();
1601 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
1602 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
1603 assert_eq!(response.decision(), Some(Decision::Allow));
1604 let errors: Vec<_> = response.errored().collect();
1605 assert_eq!(errors.len(), 0, "{errors:?}");
1606 });
1607 }
1608
1609 #[track_caller]
1610 fn assert_is_not_authorized_json_partial(call: serde_json::Value) {
1611 let ans_val = is_authorized_partial_json(call).unwrap();
1612 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
1613 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
1614 assert_eq!(response.decision(), Some(Decision::Deny));
1615 let errors: Vec<_> = response.errored().collect();
1616 assert_eq!(errors.len(), 0, "{errors:?}");
1617 });
1618 }
1619
1620 #[track_caller]
1621 fn assert_is_residual(call: serde_json::Value, expected_residuals: &HashSet<&str>) {
1622 let ans_val = is_authorized_partial_json(call).unwrap();
1623 let result: Result<PartialAuthorizationAnswer, _> = serde_json::from_value(ans_val);
1624 assert_matches!(result, Ok(PartialAuthorizationAnswer::Residuals { response, .. }) => {
1625 assert_eq!(response.decision(), None);
1626 let errors: Vec<_> = response.errored().collect();
1627 assert_eq!(errors.len(), 0, "{errors:?}");
1628 let actual_residuals: HashSet<_> = response.nontrivial_residual_ids().collect();
1629 for id in expected_residuals {
1630 assert!(actual_residuals.contains(&PolicyId::new(id)), "expected nontrivial residual for {id}, but it's missing");
1631 }
1632 for id in &actual_residuals {
1633 assert!(expected_residuals.contains(id.to_string().as_str()),"found unexpected nontrivial residual for {id}");
1634 }
1635 });
1636 }
1637
1638 #[test]
1639 fn test_authorized_partial_no_resource() {
1640 let call = json!({
1641 "principal": {
1642 "type": "User",
1643 "id": "alice"
1644 },
1645 "action": {
1646 "type": "Photo",
1647 "id": "view"
1648 },
1649 "context": {},
1650 "policies": {
1651 "staticPolicies": {
1652 "ID1": "permit(principal == User::\"alice\", action, resource);"
1653 }
1654 },
1655 "entities": []
1656 });
1657
1658 assert_is_authorized_json_partial(call);
1659 }
1660
1661 #[test]
1662 fn test_authorized_partial_not_authorized_no_resource() {
1663 let call = json!({
1664 "principal": {
1665 "type": "User",
1666 "id": "john"
1667 },
1668 "action": {
1669 "type": "Photo",
1670 "id": "view"
1671 },
1672 "context": {},
1673 "policies": {
1674 "staticPolicies": {
1675 "ID1": "permit(principal == User::\"alice\", action, resource);"
1676 }
1677 },
1678 "entities": []
1679 });
1680
1681 assert_is_not_authorized_json_partial(call);
1682 }
1683
1684 #[test]
1685 fn test_authorized_partial_residual_no_principal_scope() {
1686 let call = json!({
1687 "action": {
1688 "type": "Photo",
1689 "id": "view"
1690 },
1691 "resource" : {
1692 "type" : "Photo",
1693 "id" : "door"
1694 },
1695 "context": {},
1696 "policies": {
1697 "staticPolicies": {
1698 "ID1": "permit(principal == User::\"alice\", action, resource);"
1699 }
1700 },
1701 "entities": []
1702 });
1703
1704 assert_is_residual(call, &HashSet::from(["ID1"]));
1705 }
1706
1707 #[test]
1708 fn test_authorized_partial_residual_no_principal_when() {
1709 let call = json!({
1710 "action": {
1711 "type": "Photo",
1712 "id": "view"
1713 },
1714 "resource" : {
1715 "type" : "Photo",
1716 "id" : "door"
1717 },
1718 "context": {},
1719 "policies" : {
1720 "staticPolicies" : {
1721 "ID1": "permit(principal, action, resource) when { principal == User::\"alice\" };"
1722 }
1723 },
1724 "entities": []
1725 });
1726
1727 assert_is_residual(call, &HashSet::from(["ID1"]));
1728 }
1729
1730 #[test]
1731 fn test_authorized_partial_residual_no_principal_ignored_forbid() {
1732 let call = json!({
1733 "action": {
1734 "type": "Photo",
1735 "id": "view"
1736 },
1737 "resource" : {
1738 "type" : "Photo",
1739 "id" : "door"
1740 },
1741 "context": {},
1742 "policies" : {
1743 "staticPolicies" : {
1744 "ID1": "permit(principal, action, resource) when { principal == User::\"alice\" };",
1745 "ID2": "forbid(principal, action, resource) unless { resource == Photo::\"door\" };"
1746 }
1747 },
1748 "entities": []
1749 });
1750
1751 assert_is_residual(call, &HashSet::from(["ID1"]));
1752 }
1753}