cedar_policy_core/ast/
restricted_expr.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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 super::{Expr, ExprKind, Literal, Name};
18use crate::entities::JsonSerializationError;
19use crate::parser;
20use serde::{Deserialize, Serialize};
21use smol_str::SmolStr;
22use std::hash::{Hash, Hasher};
23use std::ops::Deref;
24use thiserror::Error;
25
26/// A few places in Core use these "restricted expressions" (for lack of a
27/// better term) which are in some sense the minimal subset of `Expr` required
28/// to express all possible `Value`s.
29///
30/// Specifically, "restricted" expressions are
31/// defined as expressions containing only the following:
32///   - bool, int, and string literals
33///   - literal EntityUIDs such as User::"alice"
34///   - extension function calls, where the arguments must be other things
35///       on this list
36///   - set and record literals, where the values must be other things on
37///       this list
38///
39/// That means the following are not allowed in "restricted" expressions:
40///   - `principal`, `action`, `resource`, `context`
41///   - builtin operators and functions, including `.`, `in`, `has`, `like`,
42///       `.contains()`
43///   - if-then-else expressions
44///
45/// These restrictions represent the expressions that are allowed to appear as
46/// attribute values in `Slice` and `Context`.
47#[derive(Deserialize, Serialize, Hash, Debug, Clone, PartialEq, Eq)]
48#[serde(transparent)]
49pub struct RestrictedExpr(Expr);
50
51impl RestrictedExpr {
52    /// Create a new `RestrictedExpr` from an `Expr`.
53    ///
54    /// This function is "safe" in the sense that it will verify that the
55    /// provided `expr` does indeed qualify as a "restricted" expression,
56    /// returning an error if not.
57    ///
58    /// Note this check requires recursively walking the AST. For a version of
59    /// this function that doesn't perform this check, see `new_unchecked()`
60    /// below.
61    pub fn new(expr: Expr) -> Result<Self, RestrictedExpressionError> {
62        is_restricted(&expr)?;
63        Ok(Self(expr))
64    }
65
66    /// Create a new `RestrictedExpr` from an `Expr`, where the caller is
67    /// responsible for ensuring that the `Expr` is a valid "restricted
68    /// expression". If it is not, internal invariants will be violated, which
69    /// may lead to other errors later, panics, or even incorrect results.
70    ///
71    /// For a "safer" version of this function that returns an error for invalid
72    /// inputs, see `new()` above.
73    pub fn new_unchecked(expr: Expr) -> Self {
74        // in debug builds, this does the check anyway, panicking if it fails
75        if cfg!(debug_assertions) {
76            // PANIC SAFETY: We're in debug mode and panicking intentionally
77            #[allow(clippy::unwrap_used)]
78            Self::new(expr).unwrap()
79        } else {
80            Self(expr)
81        }
82    }
83
84    /// Create a `RestrictedExpr` that's just a single `Literal`.
85    ///
86    /// Note that you can pass this a `Literal`, an `i64`, a `String`, etc.
87    pub fn val(v: impl Into<Literal>) -> Self {
88        // All literals are valid restricted-exprs
89        Self::new_unchecked(Expr::val(v))
90    }
91
92    /// Create a `RestrictedExpr` which evaluates to a Set of the given `RestrictedExpr`s
93    pub fn set(exprs: impl IntoIterator<Item = RestrictedExpr>) -> Self {
94        // Set expressions are valid restricted-exprs if their elements are; and
95        // we know the elements are because we require `RestrictedExpr`s in the
96        // parameter
97        Self::new_unchecked(Expr::set(exprs.into_iter().map(Into::into)))
98    }
99
100    /// Create a `RestrictedExpr` which evaluates to a Record with the given (key, value) pairs.
101    pub fn record(pairs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>) -> Self {
102        // Record expressions are valid restricted-exprs if their elements are;
103        // and we know the elements are because we require `RestrictedExpr`s in
104        // the parameter
105        Self::new_unchecked(Expr::record(pairs.into_iter().map(|(k, v)| (k, v.into()))))
106    }
107
108    /// Create a `RestrictedExpr` which calls the given extension function
109    pub fn call_extension_fn(function_name: Name, args: Vec<RestrictedExpr>) -> Self {
110        // Extension-function calls are valid restricted-exprs if their
111        // arguments are; and we know the arguments are because we require
112        // `RestrictedExpr`s in the parameter
113        Self::new_unchecked(Expr::call_extension_fn(
114            function_name,
115            args.into_iter().map(Into::into).collect(),
116        ))
117    }
118}
119
120impl std::str::FromStr for RestrictedExpr {
121    type Err = Vec<parser::err::ParseError>;
122
123    fn from_str(s: &str) -> Result<RestrictedExpr, Self::Err> {
124        parser::parse_restrictedexpr(s)
125    }
126}
127
128/// While `RestrictedExpr` wraps an _owned_ `Expr`, `BorrowedRestrictedExpr`
129/// wraps a _borrowed_ `Expr`, with the same invariants.
130#[derive(Serialize, Hash, Debug, Clone, PartialEq, Eq)]
131pub struct BorrowedRestrictedExpr<'a>(&'a Expr);
132
133impl<'a> BorrowedRestrictedExpr<'a> {
134    /// Create a new `BorrowedRestrictedExpr` from an `&Expr`.
135    ///
136    /// This function is "safe" in the sense that it will verify that the
137    /// provided `expr` does indeed qualify as a "restricted" expression,
138    /// returning an error if not.
139    ///
140    /// Note this check requires recursively walking the AST. For a version of
141    /// this function that doesn't perform this check, see `new_unchecked()`
142    /// below.
143    pub fn new(expr: &'a Expr) -> Result<Self, RestrictedExpressionError> {
144        is_restricted(expr)?;
145        Ok(Self(expr))
146    }
147
148    /// Create a new `BorrowedRestrictedExpr` from an `&Expr`, where the caller
149    /// is responsible for ensuring that the `Expr` is a valid "restricted
150    /// expression". If it is not, internal invariants will be violated, which
151    /// may lead to other errors later, panics, or even incorrect results.
152    ///
153    /// For a "safer" version of this function that returns an error for invalid
154    /// inputs, see `new()` above.
155    pub fn new_unchecked(expr: &'a Expr) -> Self {
156        // in debug builds, this does the check anyway, panicking if it fails
157        if cfg!(debug_assertions) {
158            // PANIC SAFETY: We're in debug mode and panicking intentionally
159            #[allow(clippy::unwrap_used)]
160            Self::new(expr).unwrap()
161        } else {
162            Self(expr)
163        }
164    }
165
166    /// Write a BorrowedRestrictedExpr in "natural JSON" format.
167    ///
168    /// Used to output the context as a map from Strings to JSON Values
169    pub fn to_natural_json(self) -> Result<serde_json::Value, JsonSerializationError> {
170        Ok(serde_json::to_value(
171            crate::entities::JSONValue::from_expr(self)?,
172        )?)
173    }
174}
175
176/// Helper function: does the given `Expr` qualify as a "restricted" expression.
177///
178/// Returns `Ok(())` if yes, or a `RestrictedExpressionError` if no.
179fn is_restricted(expr: &Expr) -> Result<(), RestrictedExpressionError> {
180    match expr.expr_kind() {
181        ExprKind::Lit(_) => Ok(()),
182        ExprKind::Unknown { .. } => Ok(()),
183        ExprKind::Var(_) => Err(RestrictedExpressionError::InvalidRestrictedExpression {
184            feature: expr.to_string(),
185        }),
186        ExprKind::Slot(_) => Err(RestrictedExpressionError::InvalidRestrictedExpression {
187            feature: "template slots".into(),
188        }),
189        ExprKind::If { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
190            feature: "if-then-else".into(),
191        }),
192        ExprKind::And { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
193            feature: "&&".into(),
194        }),
195        ExprKind::Or { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
196            feature: "||".into(),
197        }),
198        ExprKind::UnaryApp { op, .. } => {
199            Err(RestrictedExpressionError::InvalidRestrictedExpression {
200                feature: op.to_string(),
201            })
202        }
203        ExprKind::BinaryApp { op, .. } => {
204            Err(RestrictedExpressionError::InvalidRestrictedExpression {
205                feature: op.to_string(),
206            })
207        }
208        ExprKind::MulByConst { .. } => {
209            Err(RestrictedExpressionError::InvalidRestrictedExpression {
210                feature: "multiplication".into(),
211            })
212        }
213        ExprKind::GetAttr { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
214            feature: "get-attribute".into(),
215        }),
216        ExprKind::HasAttr { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
217            feature: "'has'".into(),
218        }),
219        ExprKind::Like { .. } => Err(RestrictedExpressionError::InvalidRestrictedExpression {
220            feature: "'like'".into(),
221        }),
222        ExprKind::ExtensionFunctionApp { args, .. } => args.iter().try_for_each(is_restricted),
223        ExprKind::Set(exprs) => exprs.iter().try_for_each(is_restricted),
224        ExprKind::Record { pairs } => pairs.iter().map(|(_, v)| v).try_for_each(is_restricted),
225    }
226}
227
228// converting into Expr is always safe; restricted exprs are always valid Exprs
229impl From<RestrictedExpr> for Expr {
230    fn from(r: RestrictedExpr) -> Expr {
231        r.0
232    }
233}
234
235impl AsRef<Expr> for RestrictedExpr {
236    fn as_ref(&self) -> &Expr {
237        &self.0
238    }
239}
240
241impl Deref for RestrictedExpr {
242    type Target = Expr;
243    fn deref(&self) -> &Expr {
244        self.as_ref()
245    }
246}
247
248impl std::fmt::Display for RestrictedExpr {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        write!(f, "{}", &self.0)
251    }
252}
253
254// converting into Expr is always safe; restricted exprs are always valid Exprs
255impl<'a> From<BorrowedRestrictedExpr<'a>> for &'a Expr {
256    fn from(r: BorrowedRestrictedExpr<'a>) -> &'a Expr {
257        r.0
258    }
259}
260
261impl<'a> AsRef<Expr> for BorrowedRestrictedExpr<'a> {
262    fn as_ref(&self) -> &Expr {
263        self.0
264    }
265}
266
267impl RestrictedExpr {
268    /// Turn an `&RestrictedExpr` into a `BorrowedRestrictedExpr`
269    pub fn as_borrowed(&self) -> BorrowedRestrictedExpr<'_> {
270        BorrowedRestrictedExpr::new_unchecked(self.as_ref())
271    }
272}
273
274impl<'a> Deref for BorrowedRestrictedExpr<'a> {
275    type Target = Expr;
276    fn deref(&self) -> &Expr {
277        self.as_ref()
278    }
279}
280
281impl<'a> std::fmt::Display for BorrowedRestrictedExpr<'a> {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        write!(f, "{}", &self.0)
284    }
285}
286
287/// Like `ExprShapeOnly`, but for restricted expressions.
288///
289/// A newtype wrapper around (borrowed) restricted expressions that provides
290/// `Eq` and `Hash` implementations that ignore any source information or other
291/// generic data used to annotate the expression.
292#[derive(Eq, Debug, Clone)]
293pub struct RestrictedExprShapeOnly<'a>(BorrowedRestrictedExpr<'a>);
294
295impl<'a> RestrictedExprShapeOnly<'a> {
296    /// Construct a `RestrictedExprShapeOnly` from a `BorrowedRestrictedExpr`.
297    /// The `BorrowedRestrictedExpr` is not modified, but any comparisons on the
298    /// resulting `RestrictedExprShapeOnly` will ignore source information and
299    /// generic data.
300    pub fn new(e: BorrowedRestrictedExpr<'a>) -> RestrictedExprShapeOnly<'a> {
301        RestrictedExprShapeOnly(e)
302    }
303}
304
305impl<'a> PartialEq for RestrictedExprShapeOnly<'a> {
306    fn eq(&self, other: &Self) -> bool {
307        self.0.eq_shape(&other.0)
308    }
309}
310
311impl<'a> Hash for RestrictedExprShapeOnly<'a> {
312    fn hash<H: Hasher>(&self, state: &mut H) {
313        self.0.hash_shape(state);
314    }
315}
316
317/// Errors generated in the restricted_expr module
318#[derive(Debug, Clone, PartialEq, Hash, Error)]
319pub enum RestrictedExpressionError {
320    /// A "restricted" expression contained a feature that is not allowed
321    /// in "restricted" expressions. The `feature` is just a string description
322    /// of the feature that is not allowed.
323    #[error("not allowed to use {feature} in a restricted expression")]
324    InvalidRestrictedExpression {
325        /// what disallowed feature appeared in the expression
326        feature: String,
327    },
328}