cedar_policy_validator/schema/
action.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//! This module contains the definition of `ValidatorActionId` and the types it relies on
18
19use cedar_policy_core::{
20    ast::{self, EntityType, EntityUID, PartialValueSerializedAsExpr},
21    transitive_closure::TCNode,
22};
23use itertools::Itertools;
24use nonempty::NonEmpty;
25use serde::Serialize;
26use smol_str::SmolStr;
27use std::collections::{BTreeMap, HashSet};
28
29use super::internal_name_to_entity_type;
30use crate::{
31    schema::{AllDefs, SchemaError},
32    types::{Attributes, Type},
33    ConditionalName,
34};
35
36/// Contains information about actions used by the validator.  The contents of
37/// the struct are the same as the schema entity type structure, but the
38/// `member_of` relation is reversed to instead be `descendants`.
39#[derive(Clone, Debug, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct ValidatorActionId {
42    /// The name of the action.
43    pub(crate) name: EntityUID,
44
45    /// The principals and resources that the action can be applied to.
46    pub(crate) applies_to: ValidatorApplySpec<ast::EntityType>,
47
48    /// The set of actions that can be members of this action. When this
49    /// structure is initially constructed, the field will contain direct
50    /// children, but it will be updated to contain the closure of all
51    /// descendants before it is used in any validation.
52    pub(crate) descendants: HashSet<EntityUID>,
53
54    /// The type of the context record associated with this action.
55    pub(crate) context: Type,
56
57    /// The attribute types for this action, used for typechecking.
58    pub(crate) attribute_types: Attributes,
59
60    /// The actual attribute value for this action, used to construct an
61    /// `Entity` for this action. Could also be used for more precise
62    /// typechecking by partial evaluation.
63    ///
64    /// Attributes are serialized as `RestrictedExpr`s, so that roundtripping
65    /// works seamlessly.
66    pub(crate) attributes: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
67}
68
69impl ValidatorActionId {
70    /// Returns an iterator over all the principals that this action applies to
71    pub fn principals(&self) -> impl Iterator<Item = &EntityType> {
72        self.applies_to.principal_apply_spec.iter()
73    }
74
75    /// Returns an iterator over all the resources that this action applies to
76    pub fn resources(&self) -> impl Iterator<Item = &EntityType> {
77        self.applies_to.resource_apply_spec.iter()
78    }
79
80    /// The `Type` that this action requires for its context.
81    ///
82    /// This always returns a closed record type.
83    pub fn context_type(&self) -> &Type {
84        &self.context
85    }
86
87    /// The [`ast::EntityType`]s that can be the `principal` for this action.
88    pub fn applies_to_principals(&self) -> impl Iterator<Item = &ast::EntityType> {
89        self.applies_to.applicable_principal_types()
90    }
91
92    /// The [`ast::EntityType`]s that can be the `resource` for this action.
93    pub fn applies_to_resources(&self) -> impl Iterator<Item = &ast::EntityType> {
94        self.applies_to.applicable_resource_types()
95    }
96
97    /// Is the given principal type applicable for this spec?
98    pub fn is_applicable_principal_type(&self, ty: &ast::EntityType) -> bool {
99        self.applies_to.is_applicable_principal_type(ty)
100    }
101
102    /// Is the given resource type applicable for this spec?
103    pub fn is_applicable_resource_type(&self, ty: &ast::EntityType) -> bool {
104        self.applies_to.is_applicable_resource_type(ty)
105    }
106}
107
108impl TCNode<EntityUID> for ValidatorActionId {
109    fn get_key(&self) -> EntityUID {
110        self.name.clone()
111    }
112
113    fn add_edge_to(&mut self, k: EntityUID) {
114        self.descendants.insert(k);
115    }
116
117    fn out_edges(&self) -> Box<dyn Iterator<Item = &EntityUID> + '_> {
118        Box::new(self.descendants.iter())
119    }
120
121    fn has_edge_to(&self, e: &EntityUID) -> bool {
122        self.descendants.contains(e)
123    }
124}
125
126/// The principals and resources that an action can be applied to.
127///
128/// The parameter `N` represents the type of entity type names stored in this
129/// [`ValidatorApplySpec`]. For instance, this could be [`crate::RawName`],
130/// [`crate::ConditionalName`], or [`InternalName`], depending on whether the
131/// names have been resolved into fully-qualified names yet.
132/// (It could also in principle be [`ast::EntityType`], which like
133/// [`InternalName`] and [`Name`] always represents a fully-qualified name, but
134/// as of this writing we always use [`Name`] or [`InternalName`] for the
135/// parameter here when we want to indicate names have been fully qualified.)
136#[derive(Clone, Debug, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub(crate) struct ValidatorApplySpec<N> {
139    /// The principal entity types the action can be applied to.
140    principal_apply_spec: HashSet<N>,
141
142    /// The resource entity types the action can be applied to.
143    resource_apply_spec: HashSet<N>,
144}
145
146impl<N> ValidatorApplySpec<N> {
147    /// Create an apply spec for an action that can only be applied to some
148    /// specific entities.
149    pub fn new(principal_apply_spec: HashSet<N>, resource_apply_spec: HashSet<N>) -> Self {
150        Self {
151            principal_apply_spec,
152            resource_apply_spec,
153        }
154    }
155}
156
157impl ValidatorApplySpec<ast::EntityType> {
158    /// Is the given principal type applicable for this spec?
159    pub fn is_applicable_principal_type(&self, ty: &ast::EntityType) -> bool {
160        self.principal_apply_spec.contains(ty)
161    }
162
163    /// Get the applicable principal types for this spec.
164    pub fn applicable_principal_types(&self) -> impl Iterator<Item = &ast::EntityType> {
165        self.principal_apply_spec.iter()
166    }
167
168    /// Is the given resource type applicable for this spec?
169    pub fn is_applicable_resource_type(&self, ty: &ast::EntityType) -> bool {
170        self.resource_apply_spec.contains(ty)
171    }
172
173    /// Get the applicable resource types for this spec.
174    pub fn applicable_resource_types(&self) -> impl Iterator<Item = &ast::EntityType> {
175        self.resource_apply_spec.iter()
176    }
177}
178
179impl ValidatorApplySpec<ConditionalName> {
180    /// Convert this [`ValidatorApplySpec<ConditionalName>`] into a
181    /// [`ValidatorApplySpec<ast::EntityType>`] by fully-qualifying all
182    /// typenames that appear anywhere in any definitions, and checking that
183    /// none of these typenames contain `__cedar`.
184    ///
185    /// `all_defs` needs to contain the full set of all fully-qualified typenames
186    /// and actions that are defined in the schema (in all schema fragments).
187    pub fn fully_qualify_type_references(
188        self,
189        all_defs: &AllDefs,
190    ) -> Result<ValidatorApplySpec<ast::EntityType>, crate::schema::SchemaError> {
191        let (principal_apply_spec, principal_errs) = self
192            .principal_apply_spec
193            .into_iter()
194            .map(|cname| {
195                let internal_name = cname.resolve(all_defs)?.clone();
196                internal_name_to_entity_type(internal_name).map_err(Into::into)
197            })
198            .partition_result::<_, Vec<SchemaError>, _, _>();
199        let (resource_apply_spec, resource_errs) = self
200            .resource_apply_spec
201            .into_iter()
202            .map(|cname| {
203                let internal_name = cname.resolve(all_defs)?.clone();
204                internal_name_to_entity_type(internal_name).map_err(Into::into)
205            })
206            .partition_result::<_, Vec<SchemaError>, _, _>();
207        match (
208            NonEmpty::from_vec(principal_errs),
209            NonEmpty::from_vec(resource_errs),
210        ) {
211            (None, None) => Ok(ValidatorApplySpec {
212                principal_apply_spec,
213                resource_apply_spec,
214            }),
215            (Some(principal_errs), None) => Err(SchemaError::join_nonempty(principal_errs)),
216            (None, Some(resource_errs)) => Err(SchemaError::join_nonempty(resource_errs)),
217            (Some(principal_errs), Some(resource_errs)) => {
218                let mut errs = principal_errs;
219                errs.extend(resource_errs);
220                Err(SchemaError::join_nonempty(errs))
221            }
222        }
223    }
224}
225
226#[cfg(test)]
227mod test {
228    use super::*;
229
230    fn make_action() -> ValidatorActionId {
231        ValidatorActionId {
232            name: r#"Action::"foo""#.parse().unwrap(),
233            applies_to: ValidatorApplySpec {
234                principal_apply_spec: HashSet::from([
235                    // Make sure duplicates are handled as expected
236                    "User".parse().unwrap(),
237                    "User".parse().unwrap(),
238                ]),
239                resource_apply_spec: HashSet::from([
240                    "App".parse().unwrap(),
241                    "File".parse().unwrap(),
242                ]),
243            },
244            descendants: HashSet::new(),
245            context: Type::any_record(),
246            attribute_types: Attributes::default(),
247            attributes: BTreeMap::default(),
248        }
249    }
250
251    #[test]
252    fn test_resources() {
253        let a = make_action();
254        let got = a.resources().cloned().collect::<HashSet<EntityType>>();
255        let expected = HashSet::from(["App".parse().unwrap(), "File".parse().unwrap()]);
256        assert_eq!(got, expected);
257    }
258
259    #[test]
260    fn test_principals() {
261        let a = make_action();
262        let got = a.principals().cloned().collect::<Vec<EntityType>>();
263        let expected: [EntityType; 1] = ["User".parse().unwrap()];
264        assert_eq!(got, &expected);
265    }
266}