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