cedar_policy_core/ast/
request.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
17use crate::entities::json::{
18    ContextJsonDeserializationError, ContextJsonParser, NullContextSchema,
19};
20use crate::evaluator::{EvaluationError, RestrictedEvaluator};
21use crate::extensions::Extensions;
22use crate::parser::Loc;
23use miette::Diagnostic;
24use serde::Serialize;
25use smol_str::SmolStr;
26use std::collections::{BTreeMap, HashMap};
27use std::sync::Arc;
28use thiserror::Error;
29
30use super::{
31    BorrowedRestrictedExpr, EntityUID, Expr, ExprKind, ExpressionConstructionError, PartialValue,
32    RestrictedExpr, Unknown, Value, ValueKind, Var,
33};
34
35/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
36#[derive(Debug, Clone, Serialize)]
37pub struct Request {
38    /// Principal associated with the request
39    pub(crate) principal: EntityUIDEntry,
40
41    /// Action associated with the request
42    pub(crate) action: EntityUIDEntry,
43
44    /// Resource associated with the request
45    pub(crate) resource: EntityUIDEntry,
46
47    /// Context associated with the request.
48    /// `None` means that variable will result in a residual for partial evaluation.
49    pub(crate) context: Option<Context>,
50}
51
52/// An entry in a request for a Entity UID.
53/// It may either be a concrete EUID
54/// or an unknown in the case of partial evaluation
55#[derive(Debug, Clone, Serialize)]
56pub enum EntityUIDEntry {
57    /// A concrete EntityUID
58    Known {
59        /// The concrete `EntityUID`
60        euid: Arc<EntityUID>,
61        /// Source location associated with the `EntityUIDEntry`, if any
62        loc: Option<Loc>,
63    },
64    /// An EntityUID left as unknown for partial evaluation
65    Unknown {
66        /// Source location associated with the `EntityUIDEntry`, if any
67        loc: Option<Loc>,
68    },
69}
70
71impl EntityUIDEntry {
72    /// Evaluate the entry to either:
73    /// A value, if the entry is concrete
74    /// An unknown corresponding to the passed `var`
75    pub fn evaluate(&self, var: Var) -> PartialValue {
76        match self {
77            EntityUIDEntry::Known { euid, loc } => {
78                Value::new(Arc::unwrap_or_clone(Arc::clone(euid)), loc.clone()).into()
79            }
80            EntityUIDEntry::Unknown { loc } => Expr::unknown(Unknown::new_untyped(var.to_string()))
81                .with_maybe_source_loc(loc.clone())
82                .into(),
83        }
84    }
85
86    /// Create an entry with a concrete EntityUID and the given source location
87    pub fn known(euid: EntityUID, loc: Option<Loc>) -> Self {
88        Self::Known {
89            euid: Arc::new(euid),
90            loc,
91        }
92    }
93
94    /// Get the UID of the entry, or `None` if it is unknown (partial evaluation)
95    pub fn uid(&self) -> Option<&EntityUID> {
96        match self {
97            Self::Known { euid, .. } => Some(euid),
98            Self::Unknown { .. } => None,
99        }
100    }
101}
102
103impl Request {
104    /// Default constructor.
105    ///
106    /// If `schema` is provided, this constructor validates that this `Request`
107    /// complies with the given `schema`.
108    pub fn new<S: RequestSchema>(
109        principal: (EntityUID, Option<Loc>),
110        action: (EntityUID, Option<Loc>),
111        resource: (EntityUID, Option<Loc>),
112        context: Context,
113        schema: Option<&S>,
114        extensions: &Extensions<'_>,
115    ) -> Result<Self, S::Error> {
116        let req = Self {
117            principal: EntityUIDEntry::known(principal.0, principal.1),
118            action: EntityUIDEntry::known(action.0, action.1),
119            resource: EntityUIDEntry::known(resource.0, resource.1),
120            context: Some(context),
121        };
122        if let Some(schema) = schema {
123            schema.validate_request(&req, extensions)?;
124        }
125        Ok(req)
126    }
127
128    /// Create a new `Request` with potentially unknown (for partial eval) variables.
129    ///
130    /// If `schema` is provided, this constructor validates that this `Request`
131    /// complies with the given `schema` (at least to the extent that we can
132    /// validate with the given information)
133    pub fn new_with_unknowns<S: RequestSchema>(
134        principal: EntityUIDEntry,
135        action: EntityUIDEntry,
136        resource: EntityUIDEntry,
137        context: Option<Context>,
138        schema: Option<&S>,
139        extensions: &Extensions<'_>,
140    ) -> Result<Self, S::Error> {
141        let req = Self {
142            principal,
143            action,
144            resource,
145            context,
146        };
147        if let Some(schema) = schema {
148            schema.validate_request(&req, extensions)?;
149        }
150        Ok(req)
151    }
152
153    /// Create a new `Request` with potentially unknown (for partial eval) variables/context
154    /// and without schema validation.
155    pub fn new_unchecked(
156        principal: EntityUIDEntry,
157        action: EntityUIDEntry,
158        resource: EntityUIDEntry,
159        context: Option<Context>,
160    ) -> Self {
161        Self {
162            principal,
163            action,
164            resource,
165            context,
166        }
167    }
168
169    /// Get the principal associated with the request
170    pub fn principal(&self) -> &EntityUIDEntry {
171        &self.principal
172    }
173
174    /// Get the action associated with the request
175    pub fn action(&self) -> &EntityUIDEntry {
176        &self.action
177    }
178
179    /// Get the resource associated with the request
180    pub fn resource(&self) -> &EntityUIDEntry {
181        &self.resource
182    }
183
184    /// Get the context associated with the request
185    /// Returning `None` means the variable is unknown, and will result in a residual expression
186    pub fn context(&self) -> Option<&Context> {
187        self.context.as_ref()
188    }
189}
190
191impl std::fmt::Display for Request {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        let display_euid = |maybe_euid: &EntityUIDEntry| match maybe_euid {
194            EntityUIDEntry::Known { euid, .. } => format!("{euid}"),
195            EntityUIDEntry::Unknown { .. } => "unknown".to_string(),
196        };
197        write!(
198            f,
199            "request with principal {}, action {}, resource {}, and context {}",
200            display_euid(&self.principal),
201            display_euid(&self.action),
202            display_euid(&self.resource),
203            match &self.context {
204                Some(x) => format!("{x}"),
205                None => "unknown".to_string(),
206            }
207        )
208    }
209}
210
211/// `Context` field of a `Request`
212#[derive(Debug, Clone, PartialEq, Serialize)]
213// Serialization is used for differential testing, which requires that `Context`
214// is serialized as a `RestrictedExpr`.
215#[serde(into = "RestrictedExpr")]
216pub enum Context {
217    /// The context is a concrete value.
218    Value(Arc<BTreeMap<SmolStr, Value>>),
219    /// The context is a residual expression, containing some unknown value in
220    /// the record attributes.
221    /// INVARIANT(restricted): Each `Expr` in this map must be a `RestrictedExpr`.
222    /// INVARIANT(unknown): At least one `Expr` must contain an `unknown`.
223    RestrictedResidual(Arc<BTreeMap<SmolStr, Expr>>),
224}
225
226impl Context {
227    /// Create an empty `Context`
228    pub fn empty() -> Self {
229        Self::Value(Arc::new(BTreeMap::new()))
230    }
231
232    /// Create a `Context` from a `PartialValue` without checking that the
233    /// residual is a restricted expression.  This function does check that the
234    /// value or residual is a record and returns `Err` when it is not.
235    ///
236    /// INVARIANT: if `value` is a residual, then it must be a valid restricted expression.
237    fn from_restricted_partial_val_unchecked(
238        value: PartialValue,
239    ) -> Result<Self, ContextCreationError> {
240        match value {
241            PartialValue::Value(v) => {
242                if let ValueKind::Record(attrs) = v.value {
243                    Ok(Context::Value(attrs))
244                } else {
245                    Err(ContextCreationError::not_a_record(v.into()))
246                }
247            }
248            PartialValue::Residual(e) => {
249                if let ExprKind::Record(attrs) = e.expr_kind() {
250                    // From the invariant on `PartialValue::Residual`, there is
251                    // an unknown in `e`. It is a record, so there must be an
252                    // unknown in one of the attributes expressions, satisfying
253                    // INVARIANT(unknown). From the invariant on this function,
254                    // `e` is a valid restricted expression, satisfying
255                    // INVARIANT(restricted).
256                    Ok(Context::RestrictedResidual(attrs.clone()))
257                } else {
258                    Err(ContextCreationError::not_a_record(e))
259                }
260            }
261        }
262    }
263
264    /// Create a `Context` from a `RestrictedExpr`, which must be a `Record`.
265    ///
266    /// `extensions` provides the `Extensions` which should be active for
267    /// evaluating the `RestrictedExpr`.
268    pub fn from_expr(
269        expr: BorrowedRestrictedExpr<'_>,
270        extensions: &Extensions<'_>,
271    ) -> Result<Self, ContextCreationError> {
272        match expr.expr_kind() {
273            ExprKind::Record { .. } => {
274                let evaluator = RestrictedEvaluator::new(extensions);
275                let pval = evaluator.partial_interpret(expr)?;
276                // The invariant on `from_restricted_partial_val_unchecked`
277                // is satisfied because `expr` is a restricted expression,
278                // and must still be restricted after `partial_interpret`.
279                // The function call cannot return `Err` because `expr` is a
280                // record, and partially evaluating a record expression will
281                // yield a record expression or a record value.
282                // PANIC SAFETY: See above
283                #[allow(clippy::expect_used)]
284                Ok(Self::from_restricted_partial_val_unchecked(pval).expect(
285                    "`from_restricted_partial_val_unchecked` should succeed when called on a record.",
286                ))
287            }
288            _ => Err(ContextCreationError::not_a_record(expr.to_owned().into())),
289        }
290    }
291
292    /// Create a `Context` from a map of key to `RestrictedExpr`, or a Vec of
293    /// `(key, RestrictedExpr)` pairs, or any other iterator of `(key, RestrictedExpr)` pairs
294    ///
295    /// `extensions` provides the `Extensions` which should be active for
296    /// evaluating the `RestrictedExpr`.
297    pub fn from_pairs(
298        pairs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
299        extensions: &Extensions<'_>,
300    ) -> Result<Self, ContextCreationError> {
301        match RestrictedExpr::record(pairs) {
302            Ok(record) => Self::from_expr(record.as_borrowed(), extensions),
303            Err(ExpressionConstructionError::DuplicateKey(err)) => Err(
304                ExpressionConstructionError::DuplicateKey(err.with_context("in context")).into(),
305            ),
306        }
307    }
308
309    /// Create a `Context` from a string containing JSON (which must be a JSON
310    /// object, not any other JSON type, or you will get an error here).
311    /// JSON here must use the `__entity` and `__extn` escapes for entity
312    /// references, extension values, etc.
313    ///
314    /// For schema-based parsing, use `ContextJsonParser`.
315    pub fn from_json_str(json: &str) -> Result<Self, ContextJsonDeserializationError> {
316        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
317            .from_json_str(json)
318    }
319
320    /// Create a `Context` from a `serde_json::Value` (which must be a JSON
321    /// object, not any other JSON type, or you will get an error here).
322    /// JSON here must use the `__entity` and `__extn` escapes for entity
323    /// references, extension values, etc.
324    ///
325    /// For schema-based parsing, use `ContextJsonParser`.
326    pub fn from_json_value(
327        json: serde_json::Value,
328    ) -> Result<Self, ContextJsonDeserializationError> {
329        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
330            .from_json_value(json)
331    }
332
333    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
334    /// object, not any other JSON type, or you will get an error here.
335    /// JSON here must use the `__entity` and `__extn` escapes for entity
336    /// references, extension values, etc.
337    ///
338    /// For schema-based parsing, use `ContextJsonParser`.
339    pub fn from_json_file(
340        json: impl std::io::Read,
341    ) -> Result<Self, ContextJsonDeserializationError> {
342        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
343            .from_json_file(json)
344    }
345
346    /// Private helper function to implement `into_iter()` for `Context`.
347    /// Gets an iterator over the (key, value) pairs in the `Context`, cloning
348    /// only if necessary.
349    fn into_values(self) -> Box<dyn Iterator<Item = (SmolStr, RestrictedExpr)>> {
350        match self {
351            Context::Value(record) => Box::new(
352                Arc::unwrap_or_clone(record)
353                    .into_iter()
354                    .map(|(k, v)| (k, RestrictedExpr::from(v))),
355            ),
356            Context::RestrictedResidual(record) => Box::new(
357                Arc::unwrap_or_clone(record)
358                    .into_iter()
359                    // By INVARIANT(restricted), all attributes expressions are
360                    // restricted expressions.
361                    .map(|(k, v)| (k, RestrictedExpr::new_unchecked(v))),
362            ),
363        }
364    }
365
366    /// Substitute unknowns with concrete values in this context. If this is
367    /// already a `Context::Value`, then this returns `self` unchanged and will
368    /// not error. Otherwise delegate to [`Expr::substitute`].
369    pub fn substitute(self, mapping: &HashMap<SmolStr, Value>) -> Result<Self, EvaluationError> {
370        match self {
371            Context::RestrictedResidual(residual_context) => {
372                // From Invariant(Restricted), `residual_context` contains only
373                // restricted expressions, so `Expr::record_arc` of the attributes
374                // will also be a restricted expression. This doesn't change after
375                // substitution, so we know `expr` must be a restricted expression.
376                let expr = Expr::record_arc(residual_context).substitute(mapping);
377                let expr = BorrowedRestrictedExpr::new_unchecked(&expr);
378
379                let extns = Extensions::all_available();
380                let eval = RestrictedEvaluator::new(extns);
381                let partial_value = eval.partial_interpret(expr)?;
382
383                // The invariant on `from_restricted_partial_val_unchecked`
384                // is satisfied because `expr` is restricted and must still be
385                // restricted after `partial_interpret`.
386                // The function call cannot fail because because `expr` was
387                // constructed as a record, and substitution and partial
388                // evaluation does not change this.
389                // PANIC SAFETY: See above
390                #[allow(clippy::expect_used)]
391                Ok(
392                    Self::from_restricted_partial_val_unchecked(partial_value).expect(
393                        "`from_restricted_partial_val_unchecked` should succeed when called on a record.",
394                    ),
395                )
396            }
397            Context::Value(_) => Ok(self),
398        }
399    }
400}
401
402/// Utilities for implementing `IntoIterator` for `Context`
403mod iter {
404    use super::*;
405
406    /// `IntoIter` iterator for `Context`
407    pub struct IntoIter(pub(super) Box<dyn Iterator<Item = (SmolStr, RestrictedExpr)>>);
408
409    impl std::fmt::Debug for IntoIter {
410        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411            write!(f, "IntoIter(<context>)")
412        }
413    }
414
415    impl Iterator for IntoIter {
416        type Item = (SmolStr, RestrictedExpr);
417
418        fn next(&mut self) -> Option<Self::Item> {
419            self.0.next()
420        }
421    }
422}
423
424impl IntoIterator for Context {
425    type Item = (SmolStr, RestrictedExpr);
426
427    type IntoIter = iter::IntoIter;
428
429    fn into_iter(self) -> Self::IntoIter {
430        iter::IntoIter(self.into_values())
431    }
432}
433
434impl From<Context> for RestrictedExpr {
435    fn from(value: Context) -> Self {
436        match value {
437            Context::Value(attrs) => Value::record_arc(attrs, None).into(),
438            Context::RestrictedResidual(attrs) => {
439                // By INVARIANT(restricted), all attributes expressions are
440                // restricted expressions, so the result of `record_arc` will be
441                // a restricted expression.
442                RestrictedExpr::new_unchecked(Expr::record_arc(attrs))
443            }
444        }
445    }
446}
447
448impl From<Context> for PartialValue {
449    fn from(ctx: Context) -> PartialValue {
450        match ctx {
451            Context::Value(attrs) => Value::record_arc(attrs, None).into(),
452            Context::RestrictedResidual(attrs) => {
453                // A `PartialValue::Residual` must contain an unknown in the
454                // expression. By INVARIANT(unknown), at least one expr in
455                // `attrs` contains an unknown, so the `record_arc` expression
456                // contains at least one unknown.
457                PartialValue::Residual(Expr::record_arc(attrs))
458            }
459        }
460    }
461}
462
463impl std::default::Default for Context {
464    fn default() -> Context {
465        Context::empty()
466    }
467}
468
469impl std::fmt::Display for Context {
470    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471        write!(f, "{}", PartialValue::from(self.clone()))
472    }
473}
474
475/// Errors while trying to create a `Context`
476#[derive(Debug, Diagnostic, Error)]
477pub enum ContextCreationError {
478    /// Tried to create a `Context` out of something other than a record
479    #[error(transparent)]
480    #[diagnostic(transparent)]
481    NotARecord(#[from] context_creation_errors::NotARecord),
482    /// Error evaluating the expression given for the `Context`
483    #[error(transparent)]
484    #[diagnostic(transparent)]
485    Evaluation(#[from] EvaluationError),
486    /// Error constructing a record for the `Context`.
487    /// Only returned by `Context::from_pairs()` and `Context::merge()`
488    #[error(transparent)]
489    #[diagnostic(transparent)]
490    ExpressionConstruction(#[from] ExpressionConstructionError),
491}
492
493impl ContextCreationError {
494    pub(crate) fn not_a_record(expr: Expr) -> Self {
495        Self::NotARecord(context_creation_errors::NotARecord {
496            expr: Box::new(expr),
497        })
498    }
499}
500
501/// Error subtypes for [`ContextCreationError`]
502pub mod context_creation_errors {
503    use super::Expr;
504    use crate::impl_diagnostic_from_expr_field;
505    use miette::Diagnostic;
506    use thiserror::Error;
507
508    /// Error type for an expression that needed to be a record, but is not
509    //
510    // CAUTION: this type is publicly exported in `cedar-policy`.
511    // Don't make fields `pub`, don't make breaking changes, and use caution
512    // when adding public methods.
513    #[derive(Debug, Error)]
514    #[error("expression is not a record: {expr}")]
515    pub struct NotARecord {
516        /// Expression which is not a record
517        pub(super) expr: Box<Expr>,
518    }
519
520    // custom impl of `Diagnostic`: take source location from the `expr` field
521    impl Diagnostic for NotARecord {
522        impl_diagnostic_from_expr_field!(expr);
523    }
524}
525
526/// Trait for schemas capable of validating `Request`s
527pub trait RequestSchema {
528    /// Error type returned when a request fails validation
529    type Error: miette::Diagnostic;
530    /// Validate the given `request`, returning `Err` if it fails validation
531    fn validate_request(
532        &self,
533        request: &Request,
534        extensions: &Extensions<'_>,
535    ) -> Result<(), Self::Error>;
536}
537
538/// A `RequestSchema` that does no validation and always reports a passing result
539#[derive(Debug, Clone)]
540pub struct RequestSchemaAllPass;
541impl RequestSchema for RequestSchemaAllPass {
542    type Error = Infallible;
543    fn validate_request(
544        &self,
545        _request: &Request,
546        _extensions: &Extensions<'_>,
547    ) -> Result<(), Self::Error> {
548        Ok(())
549    }
550}
551
552/// Wrapper around `std::convert::Infallible` which also implements
553/// `miette::Diagnostic`
554#[derive(Debug, Diagnostic, Error)]
555#[error(transparent)]
556pub struct Infallible(pub std::convert::Infallible);
557
558#[cfg(test)]
559mod test {
560    use super::*;
561    use cool_asserts::assert_matches;
562
563    #[test]
564    fn test_json_from_str_non_record() {
565        assert_matches!(
566            Context::from_expr(RestrictedExpr::val("1").as_borrowed(), Extensions::none()),
567            Err(ContextCreationError::NotARecord { .. })
568        );
569        assert_matches!(
570            Context::from_json_str("1"),
571            Err(ContextJsonDeserializationError::ContextCreation(
572                ContextCreationError::NotARecord { .. }
573            ))
574        );
575    }
576}