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::{ContextJsonDeserializationError, ContextJsonParser, NullContextSchema};
18use crate::evaluator::{EvaluationError, RestrictedEvaluator};
19use crate::extensions::Extensions;
20use crate::parser::Loc;
21use miette::Diagnostic;
22use serde::Serialize;
23use smol_str::SmolStr;
24use std::sync::Arc;
25use thiserror::Error;
26
27use super::{
28    BorrowedRestrictedExpr, EntityUID, Expr, ExprConstructionError, ExprKind, PartialValue,
29    PartialValueSerializedAsExpr, RestrictedExpr, Unknown, Value, ValueKind, Var,
30};
31
32/// Represents the request tuple <P, A, R, C> (see the Cedar design doc).
33#[derive(Debug, Clone, Serialize)]
34pub struct Request {
35    /// Principal associated with the request
36    pub(crate) principal: EntityUIDEntry,
37
38    /// Action associated with the request
39    pub(crate) action: EntityUIDEntry,
40
41    /// Resource associated with the request
42    pub(crate) resource: EntityUIDEntry,
43
44    /// Context associated with the request.
45    /// `None` means that variable will result in a residual for partial evaluation.
46    pub(crate) context: Option<Context>,
47}
48
49/// An entry in a request for a Entity UID.
50/// It may either be a concrete EUID
51/// or an unknown in the case of partial evaluation
52#[derive(Debug, Clone, Serialize)]
53pub enum EntityUIDEntry {
54    /// A concrete (but perhaps unspecified) EntityUID
55    Known {
56        /// The concrete `EntityUID`
57        euid: Arc<EntityUID>,
58        /// Source location associated with the `EntityUIDEntry`, if any
59        loc: Option<Loc>,
60    },
61    /// An EntityUID left as unknown for partial evaluation
62    Unknown {
63        /// Source location associated with the `EntityUIDEntry`, if any
64        loc: Option<Loc>,
65    },
66}
67
68impl EntityUIDEntry {
69    /// Evaluate the entry to either:
70    /// A value, if the entry is concrete
71    /// An unknown corresponding to the passed `var`
72    pub fn evaluate(&self, var: Var) -> PartialValue {
73        match self {
74            EntityUIDEntry::Known { euid, loc } => {
75                Value::new(Arc::unwrap_or_clone(Arc::clone(euid)), loc.clone()).into()
76            }
77            EntityUIDEntry::Unknown { loc } => Expr::unknown(Unknown::new_untyped(var.to_string()))
78                .with_maybe_source_loc(loc.clone())
79                .into(),
80        }
81    }
82
83    /// Create an entry with a concrete EntityUID and the given source location
84    pub fn concrete(euid: EntityUID, loc: Option<Loc>) -> Self {
85        Self::Known {
86            euid: Arc::new(euid),
87            loc,
88        }
89    }
90
91    /// Get the UID of the entry, or `None` if it is unknown (partial evaluation)
92    pub fn uid(&self) -> Option<&EntityUID> {
93        match self {
94            Self::Known { euid, .. } => Some(euid),
95            Self::Unknown { .. } => None,
96        }
97    }
98}
99
100impl Request {
101    /// Default constructor.
102    ///
103    /// If `schema` is provided, this constructor validates that this `Request`
104    /// complies with the given `schema`.
105    pub fn new<S: RequestSchema>(
106        principal: (EntityUID, Option<Loc>),
107        action: (EntityUID, Option<Loc>),
108        resource: (EntityUID, Option<Loc>),
109        context: Context,
110        schema: Option<&S>,
111        extensions: Extensions<'_>,
112    ) -> Result<Self, S::Error> {
113        let req = Self {
114            principal: EntityUIDEntry::concrete(principal.0, principal.1),
115            action: EntityUIDEntry::concrete(action.0, action.1),
116            resource: EntityUIDEntry::concrete(resource.0, resource.1),
117            context: Some(context),
118        };
119        if let Some(schema) = schema {
120            schema.validate_request(&req, extensions)?;
121        }
122        Ok(req)
123    }
124
125    /// Create a new `Request` with potentially unknown (for partial eval) variables.
126    ///
127    /// If `schema` is provided, this constructor validates that this `Request`
128    /// complies with the given `schema` (at least to the extent that we can
129    /// validate with the given information)
130    pub fn new_with_unknowns<S: RequestSchema>(
131        principal: EntityUIDEntry,
132        action: EntityUIDEntry,
133        resource: EntityUIDEntry,
134        context: Option<Context>,
135        schema: Option<&S>,
136        extensions: Extensions<'_>,
137    ) -> Result<Self, S::Error> {
138        let req = Self {
139            principal,
140            action,
141            resource,
142            context,
143        };
144        if let Some(schema) = schema {
145            schema.validate_request(&req, extensions)?;
146        }
147        Ok(req)
148    }
149
150    /// Create a new `Request` with potentially unknown (for partial eval) variables/context
151    /// and without schema validation.
152    pub fn new_unchecked(
153        principal: EntityUIDEntry,
154        action: EntityUIDEntry,
155        resource: EntityUIDEntry,
156        context: Option<Context>,
157    ) -> Self {
158        Self {
159            principal,
160            action,
161            resource,
162            context,
163        }
164    }
165
166    /// Get the principal associated with the request
167    pub fn principal(&self) -> &EntityUIDEntry {
168        &self.principal
169    }
170
171    /// Get the action associated with the request
172    pub fn action(&self) -> &EntityUIDEntry {
173        &self.action
174    }
175
176    /// Get the resource associated with the request
177    pub fn resource(&self) -> &EntityUIDEntry {
178        &self.resource
179    }
180
181    /// Get the context associated with the request
182    /// Returning `None` means the variable is unknown, and will result in a residual expression
183    pub fn context(&self) -> Option<&Context> {
184        self.context.as_ref()
185    }
186}
187
188impl std::fmt::Display for Request {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        let display_euid = |maybe_euid: &EntityUIDEntry| match maybe_euid {
191            EntityUIDEntry::Known { euid, .. } => format!("{euid}"),
192            EntityUIDEntry::Unknown { .. } => "unknown".to_string(),
193        };
194        write!(
195            f,
196            "request with principal {}, action {}, resource {}, and context {}",
197            display_euid(&self.principal),
198            display_euid(&self.action),
199            display_euid(&self.resource),
200            match &self.context {
201                Some(x) => format!("{x}"),
202                None => "unknown".to_string(),
203            }
204        )
205    }
206}
207
208/// `Context` field of a `Request`
209#[derive(Debug, Clone, PartialEq, Serialize)]
210pub struct Context {
211    /// Context is internally represented as a `PartialValue`
212    ///
213    /// Context is serialized as a `RestrictedExpr`, for partly historical reasons.
214    //
215    // INVARIANT(ContextRecord): This must be a `Record`: either
216    // `PartialValue::Value(Value::Record)` or `PartialValue::Residual(Expr::Record)`
217    #[serde(flatten)]
218    context: PartialValueSerializedAsExpr,
219}
220
221impl Context {
222    /// Create an empty `Context`
223    //
224    // INVARIANT(ContextRecord): due to use of `Value::empty_record`
225    pub fn empty() -> Self {
226        Self {
227            context: PartialValue::Value(Value::empty_record(None)).into(),
228        }
229    }
230
231    /// Create a `Context` from a `RestrictedExpr`, which must be a `Record`.
232    ///
233    /// `extensions` provides the `Extensions` which should be active for
234    /// evaluating the `RestrictedExpr`.
235    //
236    // INVARIANT(ContextRecord): always constructs a record if it returns Ok
237    pub fn from_expr(
238        expr: BorrowedRestrictedExpr<'_>,
239        extensions: Extensions<'_>,
240    ) -> Result<Self, ContextCreationError> {
241        match expr.expr_kind() {
242            // INVARIANT(ContextRecord): `RestrictedEvaluator::partial_interpret`
243            // always returns a record (or an error) given a record as input
244            ExprKind::Record { .. } => {
245                let evaluator = RestrictedEvaluator::new(&extensions);
246                let pval = evaluator.partial_interpret(expr)?;
247                Ok(Self {
248                    context: pval.into(),
249                })
250            }
251            _ => Err(ContextCreationError::NotARecord {
252                expr: Box::new(expr.to_owned()),
253            }),
254        }
255    }
256
257    /// Create a `Context` from a map of key to `RestrictedExpr`, or a Vec of
258    /// `(key, RestrictedExpr)` pairs, or any other iterator of `(key, RestrictedExpr)` pairs
259    ///
260    /// `extensions` provides the `Extensions` which should be active for
261    /// evaluating the `RestrictedExpr`.
262    pub fn from_pairs(
263        pairs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
264        extensions: Extensions<'_>,
265    ) -> Result<Self, ContextCreationError> {
266        // INVARIANT(ContextRecord): via invariant on `Self::from_expr`
267        match RestrictedExpr::record(pairs) {
268            Ok(record) => Self::from_expr(record.as_borrowed(), extensions),
269            Err(ExprConstructionError::DuplicateKey(err)) => {
270                Err(ExprConstructionError::DuplicateKey(err.with_context("in context")).into())
271            }
272        }
273    }
274
275    /// Create a `Context` from a string containing JSON (which must be a JSON
276    /// object, not any other JSON type, or you will get an error here).
277    /// JSON here must use the `__entity` and `__extn` escapes for entity
278    /// references, extension values, etc.
279    ///
280    /// For schema-based parsing, use `ContextJsonParser`.
281    pub fn from_json_str(json: &str) -> Result<Self, ContextJsonDeserializationError> {
282        // INVARIANT(ContextRecord): `.from_json_str` always produces an expression of variant `Record`
283        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
284            .from_json_str(json)
285    }
286
287    /// Create a `Context` from a `serde_json::Value` (which must be a JSON
288    /// object, not any other JSON type, or you will get an error here).
289    /// JSON here must use the `__entity` and `__extn` escapes for entity
290    /// references, extension values, etc.
291    ///
292    /// For schema-based parsing, use `ContextJsonParser`.
293    pub fn from_json_value(
294        json: serde_json::Value,
295    ) -> Result<Self, ContextJsonDeserializationError> {
296        // INVARIANT(ContextRecord): `.from_json_value` always produces an expression of variant `Record`
297        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
298            .from_json_value(json)
299    }
300
301    /// Create a `Context` from a JSON file.  The JSON file must contain a JSON
302    /// object, not any other JSON type, or you will get an error here.
303    /// JSON here must use the `__entity` and `__extn` escapes for entity
304    /// references, extension values, etc.
305    ///
306    /// For schema-based parsing, use `ContextJsonParser`.
307    pub fn from_json_file(
308        json: impl std::io::Read,
309    ) -> Result<Self, ContextJsonDeserializationError> {
310        // INVARIANT(ContextRecord): `.from_json_file` always produces an expression of variant `Record`
311        ContextJsonParser::new(None::<&NullContextSchema>, Extensions::all_available())
312            .from_json_file(json)
313    }
314
315    /// Private helper function to implement `into_iter()` for `Context`.
316    /// Gets an iterator over the (key, value) pairs in the `Context`, cloning
317    /// only if necessary.
318    //
319    // PANIC SAFETY: This is safe due to the invariant (ContextRecord) on `self.context`
320    fn into_values(self) -> Box<dyn Iterator<Item = (SmolStr, PartialValue)>> {
321        // PANIC SAFETY invariant on `self.context` ensures that it is a record
322        #[allow(clippy::panic)]
323        match self.context.into() {
324            PartialValue::Value(Value {
325                value: ValueKind::Record(record),
326                ..
327            }) => Box::new(
328                Arc::unwrap_or_clone(record)
329                    .into_iter()
330                    .map(|(k, v)| (k, PartialValue::Value(v))),
331            ),
332            PartialValue::Residual(expr) => match expr.into_expr_kind() {
333                ExprKind::Record(map) => Box::new(
334                    Arc::unwrap_or_clone(map)
335                        .into_iter()
336                        .map(|(k, v)| (k, PartialValue::Residual(v))),
337                ),
338                kind => panic!("internal invariant violation: expected a record, got {kind:?}"),
339            },
340            v => panic!("internal invariant violation: expected a record, got {v:?}"),
341        }
342    }
343}
344
345/// Utilities for implementing `IntoIterator` for `Context`
346mod iter {
347    use super::*;
348
349    /// `IntoIter` iterator for `Context`
350    pub struct IntoIter(pub(super) Box<dyn Iterator<Item = (SmolStr, PartialValue)>>);
351
352    impl std::fmt::Debug for IntoIter {
353        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354            write!(f, "IntoIter(<context>)")
355        }
356    }
357
358    impl Iterator for IntoIter {
359        type Item = (SmolStr, PartialValue);
360
361        fn next(&mut self) -> Option<Self::Item> {
362            self.0.next()
363        }
364    }
365}
366
367impl IntoIterator for Context {
368    type Item = (SmolStr, PartialValue);
369
370    type IntoIter = iter::IntoIter;
371
372    fn into_iter(self) -> Self::IntoIter {
373        iter::IntoIter(self.into_values())
374    }
375}
376
377impl AsRef<PartialValue> for Context {
378    fn as_ref(&self) -> &PartialValue {
379        &self.context
380    }
381}
382
383impl From<Context> for PartialValue {
384    fn from(ctx: Context) -> PartialValue {
385        ctx.context.into()
386    }
387}
388
389impl std::default::Default for Context {
390    fn default() -> Context {
391        Context::empty()
392    }
393}
394
395impl std::fmt::Display for Context {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        write!(f, "{}", self.context)
398    }
399}
400
401/// Errors while trying to create a `Context`
402#[derive(Debug, Diagnostic, Error)]
403pub enum ContextCreationError {
404    /// Tried to create a `Context` out of something other than a record
405    #[error("expression is not a record: `{expr}`")]
406    NotARecord {
407        /// Expression which is not a record
408        expr: Box<RestrictedExpr>,
409    },
410    /// Error evaluating the expression given for the `Context`
411    #[error(transparent)]
412    #[diagnostic(transparent)]
413    Evaluation(#[from] EvaluationError),
414    /// Error constructing a record for the `Context`.
415    /// Only returned by `Context::from_pairs()` and `Context::merge()`
416    #[error(transparent)]
417    #[diagnostic(transparent)]
418    ExprConstruction(#[from] ExprConstructionError),
419}
420
421/// Trait for schemas capable of validating `Request`s
422pub trait RequestSchema {
423    /// Error type returned when a request fails validation
424    type Error: miette::Diagnostic;
425    /// Validate the given `request`, returning `Err` if it fails validation
426    fn validate_request(
427        &self,
428        request: &Request,
429        extensions: Extensions<'_>,
430    ) -> Result<(), Self::Error>;
431}
432
433/// A `RequestSchema` that does no validation and always reports a passing result
434#[derive(Debug, Clone)]
435pub struct RequestSchemaAllPass;
436impl RequestSchema for RequestSchemaAllPass {
437    type Error = Infallible;
438    fn validate_request(
439        &self,
440        _request: &Request,
441        _extensions: Extensions<'_>,
442    ) -> Result<(), Self::Error> {
443        Ok(())
444    }
445}
446
447/// Wrapper around `std::convert::Infallible` which also implements
448/// `miette::Diagnostic`
449#[derive(Debug, Diagnostic, Error)]
450#[error(transparent)]
451pub struct Infallible(pub std::convert::Infallible);
452
453#[cfg(test)]
454mod test {
455    use super::*;
456    use cool_asserts::assert_matches;
457
458    #[test]
459    fn test_json_from_str_non_record() {
460        assert_matches!(
461            Context::from_expr(RestrictedExpr::val("1").as_borrowed(), Extensions::none()),
462            Err(ContextCreationError::NotARecord { .. })
463        );
464        assert_matches!(
465            Context::from_json_str("1"),
466            Err(ContextJsonDeserializationError::ContextCreation(
467                ContextCreationError::NotARecord { .. }
468            ))
469        );
470    }
471}