cedar_policy/ffi/
is_authorized.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! JSON FFI entry points for the Cedar authorizer. The Cedar Wasm authorizer
18//! is generated from the [`is_authorized()`] function in this file.
19
20#![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    /// Per-thread authorizer instance, initialized on first use
41    static AUTHORIZER: Authorizer = Authorizer::new();
42);
43
44/// Basic interface, using [`AuthorizationCall`] and [`AuthorizationAnswer`] types
45#[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
69/// Input is a JSON encoding of [`AuthorizationCall`] and output is a JSON
70/// encoding of [`AuthorizationAnswer`]
71///
72/// # Errors
73///
74/// Will return `Err` if the input JSON cannot be deserialized as an
75/// [`AuthorizationCall`].
76pub 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
81/// Input and output are strings containing serialized JSON, in the shapes
82/// expected by [`is_authorized_json()`]
83///
84/// # Errors
85///
86/// Will return `Err` if the input cannot be converted to valid JSON or
87/// deserialized as an [`AuthorizationCall`].
88pub 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/// Basic interface for partial evaluation, using [`AuthorizationCall`] and
94/// [`PartialAuthorizationAnswer`] types
95#[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/// Input is a JSON encoding of [`AuthorizationCall`] and output is a JSON
129/// encoding of [`PartialAuthorizationAnswer`]
130///
131/// # Errors
132///
133/// Will return `Err` if the input JSON cannot be deserialized as an
134/// [`AuthorizationCall`].
135#[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/// Input and output are strings containing serialized JSON, in the shapes
145/// expected by [`is_authorized_partial_json()`]
146///
147/// # Errors
148///
149/// Will return `Err` if the input cannot be converted to valid JSON or
150/// deserialized as an [`AuthorizationCall`].
151#[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/// Interface version of a `Response` that uses the interface version of `Diagnostics`
159#[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    /// Authorization decision
166    decision: Decision,
167    /// Diagnostics providing more information on how this decision was reached
168    diagnostics: Diagnostics,
169}
170
171/// Interface version of `Diagnostics` that stores error messages and warnings
172/// in the `DetailedError` format
173#[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    /// Ids of the policies that contributed to the decision.
180    /// If no policies applied to the request, this set will be empty.
181    reason: HashSet<PolicyId>,
182    /// Set of errors that occurred
183    errors: HashSet<AuthorizationError>,
184}
185
186impl Response {
187    /// Construct a `Response`
188    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    /// Get the authorization decision
200    pub fn decision(&self) -> Decision {
201        self.decision
202    }
203
204    /// Get the authorization diagnostics
205    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    /// Get the policies that contributed to the decision
232    pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
233        self.reason.iter()
234    }
235
236    /// Get the errors
237    pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
238        self.errors.iter()
239    }
240}
241
242/// Error (or warning) which occurred in a particular policy during authorization
243#[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    /// Id of the policy where the error (or warning) occurred
250    #[cfg_attr(feature = "wasm", tsify(type = "string"))]
251    pub policy_id: PolicyId,
252    /// Error (or warning).
253    /// You can look at the `severity` field to see whether it is actually an
254    /// error or a warning.
255    pub error: DetailedError,
256}
257
258impl AuthorizationError {
259    /// Create an `AuthorizationError` from a policy ID and any `miette` error
260    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    /// Create an `AuthorizationError` from a policy ID and a `miette::Report`
268    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/// FFI version of a [`crate::PartialResponse`]
294#[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    /// Tri-state decision
314    pub fn decision(&self) -> Option<Decision> {
315        self.decision
316    }
317
318    /// Set of all satisfied policy Ids
319    pub fn satisfied(&self) -> impl Iterator<Item = &PolicyId> {
320        self.satisfied.iter()
321    }
322
323    /// Set of all policy ids for policies that errored
324    pub fn errored(&self) -> impl Iterator<Item = &PolicyId> {
325        self.errored.iter()
326    }
327
328    /// Over approximation of policies that determine the auth decision
329    pub fn may_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
330        self.may_be_determining.iter()
331    }
332
333    /// Under approximation of policies that determine the auth decision
334    pub fn must_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
335        self.must_be_determining.iter()
336    }
337
338    /// (Borrowed) Iterator over the set of residual policies
339    pub fn residuals(&self) -> impl Iterator<Item = &JsonValueWithNoDuplicateKeys> {
340        self.residuals.values()
341    }
342
343    /// (Owned) Iterator over the set of residual policies
344    pub fn into_residuals(self) -> impl Iterator<Item = JsonValueWithNoDuplicateKeys> {
345        self.residuals.into_values()
346    }
347
348    /// Get the residual policy for a specified id if it exists
349    pub fn residual(&self, p: &PolicyId) -> Option<&JsonValueWithNoDuplicateKeys> {
350        self.residuals.get(p)
351    }
352
353    /// (Borrowed) Iterator over the set of non-trivial residual policies
354    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    ///  Iterator over the set of non-trivial residual policy ids
365    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/// Answer struct from authorization call
403#[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    /// Represents a failure to parse or call the authorizer entirely
410    #[serde(rename_all = "camelCase")]
411    Failure {
412        /// Errors encountered
413        errors: Vec<DetailedError>,
414        /// Warnings encountered
415        warnings: Vec<DetailedError>,
416    },
417    /// Represents a successful authorization call (although individual policy
418    /// evaluation may still have errors)
419    #[serde(rename_all = "camelCase")]
420    Success {
421        /// Authorization decision and diagnostics, which may include policy
422        /// evaluation errors
423        response: Response,
424        /// Warnings encountered. These are all warnings not generated by
425        /// authorization itself -- e.g. general warnings about your schema,
426        /// entity data, etc. Warnings generated by authorization are part of
427        /// `response`.
428        warnings: Vec<DetailedError>,
429    },
430}
431
432/// Answer struct from partial-authorization call
433#[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    /// Represents a failure to parse or call the authorizer entirely
441    #[serde(rename_all = "camelCase")]
442    Failure {
443        /// Errors encountered
444        errors: Vec<DetailedError>,
445        /// Warnings encountered
446        warnings: Vec<DetailedError>,
447    },
448    /// Represents a successful authorization call with either a partial or
449    /// concrete answer.  Individual policy evaluation may still have errors.
450    #[serde(rename_all = "camelCase")]
451    Residuals {
452        /// Information about the authorization decision and residuals
453        response: Box<ResidualResponse>,
454        /// Warnings encountered. These are all warnings not generated by
455        /// authorization itself -- e.g. general warnings about your schema,
456        /// entity data, etc. Warnings generated by authorization are part of
457        /// `response`.
458        warnings: Vec<DetailedError>,
459    },
460}
461
462/// Struct containing the input data for authorization
463#[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    /// The principal taking action
471    principal: EntityUid,
472    /// The action the principal is taking
473    action: EntityUid,
474    /// The resource being acted on by the principal
475    resource: EntityUid,
476    /// The context details specific to the request
477    context: Context,
478    /// Optional schema.
479    /// If present, this will inform the parsing: for instance, it will allow
480    /// `__entity` and `__extn` escapes to be implicit, and it will error if
481    /// attributes have the wrong types (e.g., string instead of integer).
482    #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
483    schema: Option<Schema>,
484    /// If this is `true` and a schema is provided, perform request validation.
485    /// If this is `false`, the schema will only be used for schema-based
486    /// parsing of `context`, and not for request validation.
487    /// If a schema is not provided, this option has no effect.
488    #[serde(default = "constant_true")]
489    validate_request: bool,
490    /// The set of policies to use during authorization
491    policies: PolicySet,
492    /// The set of entities to use during authorization
493    entities: Entities,
494}
495
496/// Struct containing the input data for partial authorization
497#[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    /// The principal taking action. If this field is empty, then the principal is unknown.
506    principal: Option<EntityUid>,
507    /// The action the principal is taking. If this field is empty, then the action is unknown.
508    action: Option<EntityUid>,
509    /// The resource being acted on by the principal. If this field is empty, then the resource is unknown.
510    resource: Option<EntityUid>,
511    /// The context details specific to the request
512    context: Context,
513    /// Optional schema.
514    /// If present, this will inform the parsing: for instance, it will allow
515    /// `__entity` and `__extn` escapes to be implicit, and it will error if
516    /// attributes have the wrong types (e.g., string instead of integer).
517    #[cfg_attr(feature = "wasm", tsify(optional, type = "Schema"))]
518    schema: Option<Schema>,
519    /// If this is `true` and a schema is provided, perform request validation.
520    /// If this is `false`, the schema will only be used for schema-based
521    /// parsing of `context`, and not for request validation.
522    /// If a schema is not provided, this option has no effect.
523    #[serde(default = "constant_true")]
524    validate_request: bool,
525    /// The set of policies to use during authorization
526    policies: PolicySet,
527    /// The set of entities to use during authorization
528    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            // At least one of the `errs.push(e)` statements above must have been reached
576            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                // At least one of the `errs.push(e)` statements above must have been reached
606                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            // At least one of the `errs.push(e)` statements above must have been reached
650            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                // At least one of the `errs.push(e)` statements above must have been reached
692                build_error(errs, warnings)
693            }
694        }
695    }
696}
697
698// PANIC SAFETY unit tests
699#[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    /// Assert that [`is_authorized_json()`] returns `Allow` with no errors
709    #[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    /// Assert that [`is_authorized_json()`] returns `Deny` with no errors
722    #[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    /// Assert that [`is_authorized_json_str()`] returns a `serde_json::Error`
735    /// error with a message that matches `msg`
736    #[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    /// Assert that [`is_authorized_json()`] returns [`AuthorizationAnswer::Failure`]
744    /// and return the enclosed errors
745    #[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        // unspecified entities are no longer supported
804        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}