cedar_policy/api.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 public library api
18#![allow(
19 clippy::missing_panics_doc,
20 clippy::missing_errors_doc,
21 clippy::similar_names,
22 clippy::result_large_err, // see #878
23)]
24
25mod id;
26#[cfg(feature = "entity-manifest")]
27use cedar_policy_core::validator::entity_manifest;
28// TODO (#1157) implement wrappers for these structs before they become public
29#[cfg(feature = "entity-manifest")]
30pub use cedar_policy_core::validator::entity_manifest::{
31 AccessTrie, EntityManifest, EntityRoot, Fields, RootAccessTrie,
32};
33use cedar_policy_core::validator::json_schema;
34use cedar_policy_core::validator::typecheck::{PolicyCheck, Typechecker};
35pub use id::*;
36
37#[cfg(feature = "deprecated-schema-compat")]
38mod deprecated_schema_compat;
39
40mod err;
41pub use err::*;
42
43pub use ast::Effect;
44pub use authorizer::Decision;
45#[cfg(feature = "partial-eval")]
46use cedar_policy_core::ast::BorrowedRestrictedExpr;
47use cedar_policy_core::ast::{self, RequestSchema, RestrictedExpr};
48use cedar_policy_core::authorizer::{self};
49use cedar_policy_core::entities::{ContextSchema, Dereference};
50use cedar_policy_core::est::{self, TemplateLink};
51use cedar_policy_core::evaluator::Evaluator;
52#[cfg(feature = "partial-eval")]
53use cedar_policy_core::evaluator::RestrictedEvaluator;
54use cedar_policy_core::extensions::Extensions;
55use cedar_policy_core::parser;
56use cedar_policy_core::FromNormalizedStr;
57use itertools::{Either, Itertools};
58use linked_hash_map::LinkedHashMap;
59use miette::Diagnostic;
60use ref_cast::RefCast;
61use serde::{Deserialize, Serialize};
62use smol_str::SmolStr;
63use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
64use std::io::Read;
65use std::str::FromStr;
66use std::sync::Arc;
67
68// PANIC SAFETY: `CARGO_PKG_VERSION` should return a valid SemVer version string
69#[allow(clippy::unwrap_used)]
70pub(crate) mod version {
71 use semver::Version;
72 use std::sync::LazyLock;
73
74 // Cedar Rust SDK Semantic Versioning version
75 static SDK_VERSION: LazyLock<Version> =
76 LazyLock::new(|| env!("CARGO_PKG_VERSION").parse().unwrap());
77 // Cedar language version
78 // The patch version field may be unnecessary
79 static LANG_VERSION: LazyLock<Version> = LazyLock::new(|| Version::new(4, 4, 0));
80
81 /// Get the Cedar SDK Semantic Versioning version
82 #[allow(clippy::module_name_repetitions)]
83 pub fn get_sdk_version() -> Version {
84 SDK_VERSION.clone()
85 }
86 /// Get the Cedar language version
87 #[allow(clippy::module_name_repetitions)]
88 pub fn get_lang_version() -> Version {
89 LANG_VERSION.clone()
90 }
91}
92
93/// Entity datatype
94#[repr(transparent)]
95#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
96pub struct Entity(pub(crate) ast::Entity);
97
98#[doc(hidden)] // because this converts to a private/internal type
99impl AsRef<ast::Entity> for Entity {
100 fn as_ref(&self) -> &ast::Entity {
101 &self.0
102 }
103}
104
105#[doc(hidden)]
106impl From<ast::Entity> for Entity {
107 fn from(entity: ast::Entity) -> Self {
108 Self(entity)
109 }
110}
111
112impl Entity {
113 /// Create a new `Entity` with this Uid, attributes, and parents (and no tags).
114 ///
115 /// Attribute values are specified here as "restricted expressions".
116 /// See docs on `RestrictedExpression`
117 /// ```
118 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
119 /// # use std::collections::{HashMap, HashSet};
120 /// # use std::str::FromStr;
121 /// let eid = EntityId::from_str("alice").unwrap();
122 /// let type_name = EntityTypeName::from_str("User").unwrap();
123 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
124 /// let attrs = HashMap::from([
125 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
126 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
127 /// ]);
128 /// let parent_eid = EntityId::from_str("admin").unwrap();
129 /// let parent_type_name = EntityTypeName::from_str("Group").unwrap();
130 /// let parent_euid = EntityUid::from_type_name_and_id(parent_type_name, parent_eid);
131 /// let parents = HashSet::from([parent_euid]);
132 /// let entity = Entity::new(euid, attrs, parents);
133 ///```
134 pub fn new(
135 uid: EntityUid,
136 attrs: HashMap<String, RestrictedExpression>,
137 parents: HashSet<EntityUid>,
138 ) -> Result<Self, EntityAttrEvaluationError> {
139 Self::new_with_tags(uid, attrs, parents, [])
140 }
141
142 /// Create a new `Entity` with no attributes or tags.
143 ///
144 /// Unlike [`Entity::new()`], this constructor cannot error.
145 /// (The only source of errors in `Entity::new()` are attributes.)
146 pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
147 // note that we take a "parents" parameter here; we will compute TC when
148 // the `Entities` object is created
149 Self(ast::Entity::new_with_attr_partial_value(
150 uid.into(),
151 [],
152 HashSet::new(),
153 parents.into_iter().map(EntityUid::into).collect(),
154 [],
155 ))
156 }
157
158 /// Create a new `Entity` with this Uid, attributes, parents, and tags.
159 ///
160 /// Attribute and tag values are specified here as "restricted expressions".
161 /// See docs on [`RestrictedExpression`].
162 pub fn new_with_tags(
163 uid: EntityUid,
164 attrs: impl IntoIterator<Item = (String, RestrictedExpression)>,
165 parents: impl IntoIterator<Item = EntityUid>,
166 tags: impl IntoIterator<Item = (String, RestrictedExpression)>,
167 ) -> Result<Self, EntityAttrEvaluationError> {
168 // note that we take a "parents" parameter here, not "ancestors"; we
169 // will compute TC when the `Entities` object is created
170 Ok(Self(ast::Entity::new(
171 uid.into(),
172 attrs.into_iter().map(|(k, v)| (k.into(), v.0)),
173 HashSet::new(),
174 parents.into_iter().map(EntityUid::into).collect(),
175 tags.into_iter().map(|(k, v)| (k.into(), v.0)),
176 Extensions::all_available(),
177 )?))
178 }
179
180 /// Create a new `Entity` with this Uid, no attributes, and no parents.
181 /// ```
182 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
183 /// # use std::str::FromStr;
184 /// let eid = EntityId::from_str("alice").unwrap();
185 /// let type_name = EntityTypeName::from_str("User").unwrap();
186 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
187 /// let alice = Entity::with_uid(euid);
188 /// # cool_asserts::assert_matches!(alice.attr("age"), None);
189 /// ```
190 pub fn with_uid(uid: EntityUid) -> Self {
191 Self(ast::Entity::with_uid(uid.into()))
192 }
193
194 /// Test if two entities are structurally equal. That is, not only do they
195 /// have the same UID, but they also have the same attributes and ancestors.
196 ///
197 /// Note that ancestor equality is determined by examining the ancestors
198 /// entities provided when constructing these objects, without computing
199 /// their transitive closure. For accurate comparison, entities should be
200 /// constructed with the transitive closure precomputed or be drawn from an
201 /// [`Entities`] object which will perform this computation.
202 pub fn deep_eq(&self, other: &Self) -> bool {
203 self.0.deep_eq(&other.0)
204 }
205
206 /// Get the Uid of this entity
207 /// ```
208 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
209 /// # use std::str::FromStr;
210 /// # let eid = EntityId::from_str("alice").unwrap();
211 /// let type_name = EntityTypeName::from_str("User").unwrap();
212 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
213 /// let alice = Entity::with_uid(euid.clone());
214 /// assert_eq!(alice.uid(), euid);
215 /// ```
216 pub fn uid(&self) -> EntityUid {
217 self.0.uid().clone().into()
218 }
219
220 /// Get the value for the given attribute, or `None` if not present.
221 ///
222 /// This can also return Some(Err) if the attribute is not a value (i.e., is
223 /// unknown due to partial evaluation).
224 /// ```
225 /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, EvalResult, RestrictedExpression};
226 /// # use std::collections::{HashMap, HashSet};
227 /// # use std::str::FromStr;
228 /// let eid = EntityId::from_str("alice").unwrap();
229 /// let type_name = EntityTypeName::from_str("User").unwrap();
230 /// let euid = EntityUid::from_type_name_and_id(type_name, eid);
231 /// let attrs = HashMap::from([
232 /// ("age".to_string(), RestrictedExpression::from_str("21").unwrap()),
233 /// ("department".to_string(), RestrictedExpression::from_str("\"CS\"").unwrap()),
234 /// ]);
235 /// let entity = Entity::new(euid, attrs, HashSet::new()).unwrap();
236 /// assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(21));
237 /// assert_eq!(entity.attr("department").unwrap().unwrap(), EvalResult::String("CS".to_string()));
238 /// assert!(entity.attr("foo").is_none());
239 /// ```
240 pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
241 match ast::Value::try_from(self.0.get(attr)?.clone()) {
242 Ok(v) => Some(Ok(EvalResult::from(v))),
243 Err(e) => Some(Err(e)),
244 }
245 }
246
247 /// Get the value for the given tag, or `None` if not present.
248 ///
249 /// This can also return Some(Err) if the tag is not a value (i.e., is
250 /// unknown due to partial evaluation).
251 pub fn tag(&self, tag: &str) -> Option<Result<EvalResult, PartialValueToValueError>> {
252 match ast::Value::try_from(self.0.get_tag(tag)?.clone()) {
253 Ok(v) => Some(Ok(EvalResult::from(v))),
254 Err(e) => Some(Err(e)),
255 }
256 }
257
258 /// Consume the entity and return the entity's owned Uid, attributes and parents.
259 pub fn into_inner(
260 self,
261 ) -> (
262 EntityUid,
263 HashMap<String, RestrictedExpression>,
264 HashSet<EntityUid>,
265 ) {
266 let (uid, attrs, ancestors, mut parents, _) = self.0.into_inner();
267 parents.extend(ancestors);
268
269 let attrs = attrs
270 .into_iter()
271 .map(|(k, v)| {
272 (
273 k.to_string(),
274 match v {
275 ast::PartialValue::Value(val) => {
276 RestrictedExpression(ast::RestrictedExpr::from(val))
277 }
278 ast::PartialValue::Residual(exp) => {
279 RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
280 }
281 },
282 )
283 })
284 .collect();
285
286 (
287 uid.into(),
288 attrs,
289 parents.into_iter().map(Into::into).collect(),
290 )
291 }
292
293 /// Parse an entity from an in-memory JSON value
294 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
295 pub fn from_json_value(
296 value: serde_json::Value,
297 schema: Option<&Schema>,
298 ) -> Result<Self, EntitiesError> {
299 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
300 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
301 schema.as_ref(),
302 Extensions::all_available(),
303 cedar_policy_core::entities::TCComputation::ComputeNow,
304 );
305 eparser.single_from_json_value(value).map(Self)
306 }
307
308 /// Parse an entity from a JSON string
309 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
310 pub fn from_json_str(
311 src: impl AsRef<str>,
312 schema: Option<&Schema>,
313 ) -> Result<Self, EntitiesError> {
314 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
315 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
316 schema.as_ref(),
317 Extensions::all_available(),
318 cedar_policy_core::entities::TCComputation::ComputeNow,
319 );
320 eparser.single_from_json_str(src).map(Self)
321 }
322
323 /// Parse an entity from a JSON reader
324 /// If a schema is provided, it is handled identically to [`Entities::from_json_str`]
325 pub fn from_json_file(f: impl Read, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
326 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
327 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
328 schema.as_ref(),
329 Extensions::all_available(),
330 cedar_policy_core::entities::TCComputation::ComputeNow,
331 );
332 eparser.single_from_json_file(f).map(Self)
333 }
334
335 /// Dump an `Entity` object into an entity JSON file.
336 ///
337 /// The resulting JSON will be suitable for parsing in via
338 /// `from_json_*`, and will be parse-able even with no [`Schema`].
339 ///
340 /// To read an `Entity` object from JSON , use
341 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
342 pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
343 self.0.write_to_json(f)
344 }
345
346 /// Dump an `Entity` object into an in-memory JSON object.
347 ///
348 /// The resulting JSON will be suitable for parsing in via
349 /// `from_json_*`, and will be parse-able even with no `Schema`.
350 ///
351 /// To read an `Entity` object from JSON , use
352 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
353 pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
354 self.0.to_json_value()
355 }
356
357 /// Dump an `Entity` object into a JSON string.
358 ///
359 /// The resulting JSON will be suitable for parsing in via
360 /// `from_json_*`, and will be parse-able even with no `Schema`.
361 ///
362 /// To read an `Entity` object from JSON , use
363 /// [`Self::from_json_file`], [`Self::from_json_value`], or [`Self::from_json_str`].
364 pub fn to_json_string(&self) -> Result<String, EntitiesError> {
365 self.0.to_json_string()
366 }
367}
368
369impl std::fmt::Display for Entity {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 write!(f, "{}", self.0)
372 }
373}
374
375/// Represents an entity hierarchy, and allows looking up `Entity` objects by
376/// Uid.
377#[repr(transparent)]
378#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
379pub struct Entities(pub(crate) cedar_policy_core::entities::Entities);
380
381#[doc(hidden)] // because this converts to a private/internal type
382impl AsRef<cedar_policy_core::entities::Entities> for Entities {
383 fn as_ref(&self) -> &cedar_policy_core::entities::Entities {
384 &self.0
385 }
386}
387
388#[doc(hidden)]
389impl From<cedar_policy_core::entities::Entities> for Entities {
390 fn from(entities: cedar_policy_core::entities::Entities) -> Self {
391 Self(entities)
392 }
393}
394
395use entities_errors::EntitiesError;
396
397impl Entities {
398 /// Create a fresh `Entities` with no entities
399 /// ```
400 /// # use cedar_policy::Entities;
401 /// let entities = Entities::empty();
402 /// # assert!(entities.is_empty());
403 /// ```
404 pub fn empty() -> Self {
405 Self(cedar_policy_core::entities::Entities::new())
406 }
407
408 /// Get the `Entity` with the given Uid, if any
409 pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
410 match self.0.entity(uid.as_ref()) {
411 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
412 Dereference::Data(e) => Some(Entity::ref_cast(e)),
413 }
414 }
415
416 /// Transform the store into a partial store, where
417 /// attempting to dereference a non-existent `EntityUid` results in
418 /// a residual instead of an error.
419 #[doc = include_str!("../experimental_warning.md")]
420 #[must_use]
421 #[cfg(feature = "partial-eval")]
422 pub fn partial(self) -> Self {
423 Self(self.0.partial())
424 }
425
426 /// Iterate over the `Entity`'s in the `Entities`
427 pub fn iter(&self) -> impl Iterator<Item = &Entity> {
428 self.0.iter().map(Entity::ref_cast)
429 }
430
431 /// Test if two entity hierarchies are structurally equal. The hierarchies
432 /// must contain the same set of entity ids, and the entities with each id
433 /// must be structurally equal (decided by [`Entity::deep_eq`]). Ancestor
434 /// equality between entities is always decided by comparing the transitive
435 /// closure of ancestor and not direct parents.
436 pub fn deep_eq(&self, other: &Self) -> bool {
437 self.0.deep_eq(&other.0)
438 }
439
440 /// Create an `Entities` object with the given entities.
441 ///
442 /// `schema` represents a source of `Action` entities, which will be added
443 /// to the entities provided.
444 /// (If any `Action` entities are present in the provided entities, and a
445 /// `schema` is also provided, each `Action` entity in the provided entities
446 /// must exactly match its definition in the schema or an error is
447 /// returned.)
448 ///
449 /// If a `schema` is present, this function will also ensure that the
450 /// produced entities fully conform to the `schema` -- for instance, it will
451 /// error if attributes have the wrong types (e.g., string instead of
452 /// integer), or if required attributes are missing or superfluous
453 /// attributes are provided.
454 /// ## Errors
455 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
456 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
457 /// to the schema
458 pub fn from_entities(
459 entities: impl IntoIterator<Item = Entity>,
460 schema: Option<&Schema>,
461 ) -> Result<Self, EntitiesError> {
462 cedar_policy_core::entities::Entities::from_entities(
463 entities.into_iter().map(|e| e.0),
464 schema
465 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
466 .as_ref(),
467 cedar_policy_core::entities::TCComputation::ComputeNow,
468 Extensions::all_available(),
469 )
470 .map(Entities)
471 }
472
473 /// Add all of the [`Entity`]s in the collection to this [`Entities`]
474 /// structure, re-computing the transitive closure.
475 ///
476 /// If a `schema` is provided, this method will ensure that the added
477 /// entities fully conform to the schema -- for instance, it will error if
478 /// attributes have the wrong types (e.g., string instead of integer), or if
479 /// required attributes are missing or superfluous attributes are provided.
480 /// (This method will not add action entities from the `schema`.)
481 ///
482 /// Re-computing the transitive closure can be expensive, so it is advised
483 /// to not call this method in a loop.
484 /// ## Errors
485 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities` with the same Entity UID,
486 /// or there is an entity in `entities` with the same Entity UID as a non-identical entity in this structure
487 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
488 /// to the schema
489 pub fn add_entities(
490 self,
491 entities: impl IntoIterator<Item = Entity>,
492 schema: Option<&Schema>,
493 ) -> Result<Self, EntitiesError> {
494 Ok(Self(
495 self.0.add_entities(
496 entities.into_iter().map(|e| Arc::new(e.0)),
497 schema
498 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
499 .as_ref(),
500 cedar_policy_core::entities::TCComputation::ComputeNow,
501 Extensions::all_available(),
502 )?,
503 ))
504 }
505
506 /// Removes each of the [`EntityUid`]s in the iterator
507 /// from this [`Entities`] structure, re-computing the transitive
508 /// closure after removing all edges to/from the removed entities.
509 ///
510 /// Re-computing the transitive closure can be expensive, so it is
511 /// advised to not call this method in a loop.
512 pub fn remove_entities(
513 self,
514 entity_ids: impl IntoIterator<Item = EntityUid>,
515 ) -> Result<Self, EntitiesError> {
516 Ok(Self(self.0.remove_entities(
517 entity_ids.into_iter().map(|euid| euid.0),
518 cedar_policy_core::entities::TCComputation::ComputeNow,
519 )?))
520 }
521
522 /// Updates or adds all of the [`Entity`]s in the collection to this [`Entities`]
523 /// structure, re-computing the transitive closure.
524 ///
525 /// If a `schema` is provided, this method will ensure that the added
526 /// entities fully conform to the schema -- for instance, it will error if
527 /// attributes have the wrong types (e.g., string instead of integer), or if
528 /// required attributes are missing or superfluous attributes are provided.
529 /// (This method will not add action entities from the `schema`.)
530 ///
531 /// Re-computing the transitive closure can be expensive, so it is advised
532 /// to not call this method in a loop.
533 /// ## Errors
534 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
535 /// to the schema
536 pub fn upsert_entities(
537 self,
538 entities: impl IntoIterator<Item = Entity>,
539 schema: Option<&Schema>,
540 ) -> Result<Self, EntitiesError> {
541 Ok(Self(
542 self.0.upsert_entities(
543 entities.into_iter().map(|e| Arc::new(e.0)),
544 schema
545 .map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0))
546 .as_ref(),
547 cedar_policy_core::entities::TCComputation::ComputeNow,
548 Extensions::all_available(),
549 )?,
550 ))
551 }
552
553 /// Parse an entities JSON file (in [&str] form) and add them into this
554 /// [`Entities`] structure, re-computing the transitive closure
555 ///
556 /// If a `schema` is provided, this will inform the parsing: for instance, it
557 /// will allow `__entity` and `__extn` escapes to be implicit.
558 /// This method will also ensure that the added entities fully conform to the
559 /// schema -- for instance, it will error if attributes have the wrong types
560 /// (e.g., string instead of integer), or if required attributes are missing
561 /// or superfluous attributes are provided.
562 /// (This method will not add action entities from the `schema`.)
563 ///
564 /// Re-computing the transitive closure can be expensive, so it is advised
565 /// to not call this method in a loop.
566 /// ## Errors
567 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
568 /// `entities` with the same Entity UID, or there is an entity in `entities` with the
569 /// same Entity UID as a non-identical entity in this structure
570 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
571 /// to the schema
572 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
573 pub fn add_entities_from_json_str(
574 self,
575 json: &str,
576 schema: Option<&Schema>,
577 ) -> Result<Self, EntitiesError> {
578 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
579 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
580 schema.as_ref(),
581 Extensions::all_available(),
582 cedar_policy_core::entities::TCComputation::ComputeNow,
583 );
584 let new_entities = eparser.iter_from_json_str(json)?.map(Arc::new);
585 Ok(Self(self.0.add_entities(
586 new_entities,
587 schema.as_ref(),
588 cedar_policy_core::entities::TCComputation::ComputeNow,
589 Extensions::all_available(),
590 )?))
591 }
592
593 /// Parse an entities JSON file (in [`serde_json::Value`] form) and add them
594 /// into this [`Entities`] structure, re-computing the transitive closure
595 ///
596 /// If a `schema` is provided, this will inform the parsing: for instance, it
597 /// will allow `__entity` and `__extn` escapes to be implicit.
598 /// This method will also ensure that the added entities fully conform to the
599 /// schema -- for instance, it will error if attributes have the wrong types
600 /// (e.g., string instead of integer), or if required attributes are missing
601 /// or superfluous attributes are provided.
602 /// (This method will not add action entities from the `schema`.)
603 ///
604 /// Re-computing the transitive closure can be expensive, so it is advised
605 /// to not call this method in a loop.
606 /// ## Errors
607 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in
608 /// `entities` with the same Entity UID, or there is an entity in `entities` with the same
609 /// Entity UID as a non-identical entity in this structure
610 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
611 /// to the schema
612 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
613 pub fn add_entities_from_json_value(
614 self,
615 json: serde_json::Value,
616 schema: Option<&Schema>,
617 ) -> Result<Self, EntitiesError> {
618 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
619 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
620 schema.as_ref(),
621 Extensions::all_available(),
622 cedar_policy_core::entities::TCComputation::ComputeNow,
623 );
624 let new_entities = eparser.iter_from_json_value(json)?.map(Arc::new);
625 Ok(Self(self.0.add_entities(
626 new_entities,
627 schema.as_ref(),
628 cedar_policy_core::entities::TCComputation::ComputeNow,
629 Extensions::all_available(),
630 )?))
631 }
632
633 /// Parse an entities JSON file (in [`std::io::Read`] form) and add them
634 /// into this [`Entities`] structure, re-computing the transitive closure
635 ///
636 /// If a `schema` is provided, this will inform the parsing: for instance, it
637 /// will allow `__entity` and `__extn` escapes to be implicit.
638 /// This method will also ensure that the added entities fully conform to the
639 /// schema -- for instance, it will error if attributes have the wrong types
640 /// (e.g., string instead of integer), or if required attributes are missing
641 /// or superfluous attributes are provided.
642 /// (This method will not add action entities from the `schema`.)
643 ///
644 /// Re-computing the transitive closure can be expensive, so it is advised
645 /// to not call this method in a loop.
646 ///
647 /// ## Errors
648 /// - [`EntitiesError::Duplicate`] if there is a pair of non-identical entities in `entities`
649 /// with the same Entity UID, or there is an entity in `entities` with the same Entity UID as a
650 /// non-identical entity in this structure
651 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
652 /// to the schema
653 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
654 pub fn add_entities_from_json_file(
655 self,
656 json: impl std::io::Read,
657 schema: Option<&Schema>,
658 ) -> Result<Self, EntitiesError> {
659 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
660 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
661 schema.as_ref(),
662 Extensions::all_available(),
663 cedar_policy_core::entities::TCComputation::ComputeNow,
664 );
665 let new_entities = eparser.iter_from_json_file(json)?.map(Arc::new);
666 Ok(Self(self.0.add_entities(
667 new_entities,
668 schema.as_ref(),
669 cedar_policy_core::entities::TCComputation::ComputeNow,
670 Extensions::all_available(),
671 )?))
672 }
673
674 /// Parse an entities JSON file (in `&str` form) into an `Entities` object
675 ///
676 /// `schema` represents a source of `Action` entities, which will be added
677 /// to the entities parsed from JSON.
678 /// (If any `Action` entities are present in the JSON, and a `schema` is
679 /// also provided, each `Action` entity in the JSON must exactly match its
680 /// definition in the schema or an error is returned.)
681 ///
682 /// If a `schema` is present, this will also inform the parsing: for
683 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
684 ///
685 /// Finally, if a `schema` is present, this function will ensure
686 /// that the produced entities fully conform to the `schema` -- for
687 /// instance, it will error if attributes have the wrong types (e.g., string
688 /// instead of integer), or if required attributes are missing or
689 /// superfluous attributes are provided.
690 ///
691 /// ## Errors
692 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
693 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
694 /// to the schema
695 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
696 ///
697 /// ```
698 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
699 /// # use std::str::FromStr;
700 /// let data =r#"
701 /// [
702 /// {
703 /// "uid": {"type":"User","id":"alice"},
704 /// "attrs": {
705 /// "age":19,
706 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
707 /// },
708 /// "parents": [{"type":"Group","id":"admin"}]
709 /// },
710 /// {
711 /// "uid": {"type":"Group","id":"admin"},
712 /// "attrs": {},
713 /// "parents": []
714 /// }
715 /// ]
716 /// "#;
717 /// let entities = Entities::from_json_str(data, None).unwrap();
718 /// # let euid = EntityUid::from_str(r#"User::"alice""#).unwrap();
719 /// # let entity = entities.get(&euid).unwrap();
720 /// # assert_eq!(entity.attr("age").unwrap().unwrap(), EvalResult::Long(19));
721 /// # let ip = entity.attr("ip_addr").unwrap().unwrap();
722 /// # assert_eq!(ip, EvalResult::ExtensionValue("ip(\"10.0.1.101\")".to_string()));
723 /// ```
724 pub fn from_json_str(json: &str, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
725 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
726 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
727 schema.as_ref(),
728 Extensions::all_available(),
729 cedar_policy_core::entities::TCComputation::ComputeNow,
730 );
731 eparser.from_json_str(json).map(Entities)
732 }
733
734 /// Parse an entities JSON file (in `serde_json::Value` form) into an
735 /// `Entities` object
736 ///
737 /// `schema` represents a source of `Action` entities, which will be added
738 /// to the entities parsed from JSON.
739 /// (If any `Action` entities are present in the JSON, and a `schema` is
740 /// also provided, each `Action` entity in the JSON must exactly match its
741 /// definition in the schema or an error is returned.)
742 ///
743 /// If a `schema` is present, this will also inform the parsing: for
744 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
745 ///
746 /// Finally, if a `schema` is present, this function will ensure
747 /// that the produced entities fully conform to the `schema` -- for
748 /// instance, it will error if attributes have the wrong types (e.g., string
749 /// instead of integer), or if required attributes are missing or
750 /// superfluous attributes are provided.
751 ///
752 /// ## Errors
753 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
754 /// - [`EntitiesError::InvalidEntity`]if `schema` is not none and any entities do not conform
755 /// to the schema
756 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
757 ///
758 /// ```
759 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, EvalResult, Request,PolicySet};
760 /// let data =serde_json::json!(
761 /// [
762 /// {
763 /// "uid": {"type":"User","id":"alice"},
764 /// "attrs": {
765 /// "age":19,
766 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
767 /// },
768 /// "parents": [{"type":"Group","id":"admin"}]
769 /// },
770 /// {
771 /// "uid": {"type":"Group","id":"admin"},
772 /// "attrs": {},
773 /// "parents": []
774 /// }
775 /// ]
776 /// );
777 /// let entities = Entities::from_json_value(data, None).unwrap();
778 /// ```
779 pub fn from_json_value(
780 json: serde_json::Value,
781 schema: Option<&Schema>,
782 ) -> Result<Self, EntitiesError> {
783 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
784 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
785 schema.as_ref(),
786 Extensions::all_available(),
787 cedar_policy_core::entities::TCComputation::ComputeNow,
788 );
789 eparser.from_json_value(json).map(Entities)
790 }
791
792 /// Parse an entities JSON file (in `std::io::Read` form) into an `Entities`
793 /// object
794 ///
795 /// `schema` represents a source of `Action` entities, which will be added
796 /// to the entities parsed from JSON.
797 /// (If any `Action` entities are present in the JSON, and a `schema` is
798 /// also provided, each `Action` entity in the JSON must exactly match its
799 /// definition in the schema or an error is returned.)
800 ///
801 /// If a `schema` is present, this will also inform the parsing: for
802 /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
803 ///
804 /// Finally, if a `schema` is present, this function will ensure
805 /// that the produced entities fully conform to the `schema` -- for
806 /// instance, it will error if attributes have the wrong types (e.g., string
807 /// instead of integer), or if required attributes are missing or
808 /// superfluous attributes are provided.
809 ///
810 /// ## Errors
811 /// - [`EntitiesError::Duplicate`] if there are any duplicate entities in `entities`
812 /// - [`EntitiesError::InvalidEntity`] if `schema` is not none and any entities do not conform
813 /// to the schema
814 /// - [`EntitiesError::Deserialization`] if there are errors while parsing the json
815 pub fn from_json_file(
816 json: impl std::io::Read,
817 schema: Option<&Schema>,
818 ) -> Result<Self, EntitiesError> {
819 let schema = schema.map(|s| cedar_policy_core::validator::CoreSchema::new(&s.0));
820 let eparser = cedar_policy_core::entities::EntityJsonParser::new(
821 schema.as_ref(),
822 Extensions::all_available(),
823 cedar_policy_core::entities::TCComputation::ComputeNow,
824 );
825 eparser.from_json_file(json).map(Entities)
826 }
827
828 /// Is entity `a` an ancestor of entity `b`?
829 /// Same semantics as `b in a` in the Cedar language
830 pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
831 match self.0.entity(b.as_ref()) {
832 Dereference::Data(b) => b.is_descendant_of(a.as_ref()),
833 _ => a == b, // if b doesn't exist, `b in a` is only true if `b == a`
834 }
835 }
836
837 /// Get an iterator over the ancestors of the given Euid.
838 /// Returns `None` if the given `Euid` does not exist.
839 pub fn ancestors<'a>(
840 &'a self,
841 euid: &EntityUid,
842 ) -> Option<impl Iterator<Item = &'a EntityUid>> {
843 let entity = match self.0.entity(euid.as_ref()) {
844 Dereference::Residual(_) | Dereference::NoSuchEntity => None,
845 Dereference::Data(e) => Some(e),
846 }?;
847 Some(entity.ancestors().map(EntityUid::ref_cast))
848 }
849
850 /// Returns the number of `Entity`s in the `Entities`
851 pub fn len(&self) -> usize {
852 self.0.len()
853 }
854
855 /// Returns true if the `Entities` contains no `Entity`s
856 pub fn is_empty(&self) -> bool {
857 self.0.is_empty()
858 }
859
860 /// Dump an `Entities` object into an entities JSON file.
861 ///
862 /// The resulting JSON will be suitable for parsing in via
863 /// `from_json_*`, and will be parse-able even with no `Schema`.
864 ///
865 /// To read an `Entities` object from an entities JSON file, use
866 /// `from_json_file`.
867 pub fn write_to_json(&self, f: impl std::io::Write) -> std::result::Result<(), EntitiesError> {
868 self.0.write_to_json(f)
869 }
870
871 #[doc = include_str!("../experimental_warning.md")]
872 /// Visualize an `Entities` object in the graphviz `dot`
873 /// format. Entity visualization is best-effort and not well tested.
874 /// Feel free to submit an issue if you are using this feature and would like it improved.
875 pub fn to_dot_str(&self) -> String {
876 let mut dot_str = String::new();
877 // PANIC SAFETY: Writing to the String `dot_str` cannot fail, so `to_dot_str` will not return an `Err` result.
878 #[allow(clippy::unwrap_used)]
879 self.0.to_dot_str(&mut dot_str).unwrap();
880 dot_str
881 }
882}
883
884/// Validates scope variables against the provided schema
885///
886/// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
887///
888/// This validation is already handled by `Request::new`, so there is no need to separately call
889/// if you are validating the whole request
890pub fn validate_scope_variables(
891 principal: &EntityUid,
892 action: &EntityUid,
893 resource: &EntityUid,
894 schema: &Schema,
895) -> std::result::Result<(), RequestValidationError> {
896 Ok(RequestSchema::validate_scope_variables(
897 &schema.0,
898 Some(&principal.0),
899 Some(&action.0),
900 Some(&resource.0),
901 )?)
902}
903
904/// Utilities for defining `IntoIterator` over `Entities`
905pub mod entities {
906
907 /// `IntoIter` iterator for `Entities`
908 #[derive(Debug)]
909 pub struct IntoIter {
910 pub(super) inner: <cedar_policy_core::entities::Entities as IntoIterator>::IntoIter,
911 }
912
913 impl Iterator for IntoIter {
914 type Item = super::Entity;
915
916 fn next(&mut self) -> Option<Self::Item> {
917 self.inner.next().map(super::Entity)
918 }
919 fn size_hint(&self) -> (usize, Option<usize>) {
920 self.inner.size_hint()
921 }
922 }
923}
924
925impl IntoIterator for Entities {
926 type Item = Entity;
927 type IntoIter = entities::IntoIter;
928
929 fn into_iter(self) -> Self::IntoIter {
930 Self::IntoIter {
931 inner: self.0.into_iter(),
932 }
933 }
934}
935
936/// Authorizer object, which provides responses to authorization queries
937#[repr(transparent)]
938#[derive(Debug, Clone, RefCast)]
939pub struct Authorizer(authorizer::Authorizer);
940
941#[doc(hidden)] // because this converts to a private/internal type
942impl AsRef<authorizer::Authorizer> for Authorizer {
943 fn as_ref(&self) -> &authorizer::Authorizer {
944 &self.0
945 }
946}
947
948impl Default for Authorizer {
949 fn default() -> Self {
950 Self::new()
951 }
952}
953
954impl Authorizer {
955 /// Create a new `Authorizer`
956 ///
957 /// The authorizer uses the `stacker` crate to manage stack size and tries to use a sane default.
958 /// If the default is not right for you, you can try wrapping the authorizer or individual calls
959 /// to `is_authorized` in `stacker::grow`.
960 /// Note that on platforms not supported by `stacker` (e.g., Wasm, Android),
961 /// the authorizer will simply assume that the stack size is sufficient. As a result, large inputs
962 /// may result in stack overflows and crashing the process.
963 /// But on all platforms supported by `stacker` (Linux, macOS, ...), Cedar will return the
964 /// graceful error `RecursionLimit` instead of crashing.
965 /// ```
966 /// # use cedar_policy::{Authorizer, Context, Entities, EntityId, EntityTypeName,
967 /// # EntityUid, Request,PolicySet};
968 /// # use std::str::FromStr;
969 /// # // create a request
970 /// # let p_eid = EntityId::from_str("alice").unwrap();
971 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
972 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
973 /// #
974 /// # let a_eid = EntityId::from_str("view").unwrap();
975 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
976 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
977 /// #
978 /// # let r_eid = EntityId::from_str("trip").unwrap();
979 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
980 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
981 /// #
982 /// # let c = Context::empty();
983 /// #
984 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
985 /// #
986 /// # // create a policy
987 /// # let s = r#"permit(
988 /// # principal == User::"alice",
989 /// # action == Action::"view",
990 /// # resource == Album::"trip"
991 /// # )when{
992 /// # principal.ip_addr.isIpv4()
993 /// # };
994 /// # "#;
995 /// # let policy = PolicySet::from_str(s).expect("policy error");
996 /// # // create entities
997 /// # let e = r#"[
998 /// # {
999 /// # "uid": {"type":"User","id":"alice"},
1000 /// # "attrs": {
1001 /// # "age":19,
1002 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1003 /// # },
1004 /// # "parents": []
1005 /// # }
1006 /// # ]"#;
1007 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1008 /// let authorizer = Authorizer::new();
1009 /// let r = authorizer.is_authorized(&request, &policy, &entities);
1010 /// ```
1011 pub fn new() -> Self {
1012 Self(authorizer::Authorizer::new())
1013 }
1014
1015 /// Returns an authorization response for `r` with respect to the given
1016 /// `PolicySet` and `Entities`.
1017 ///
1018 /// The language spec and formal model give a precise definition of how this
1019 /// is computed.
1020 /// ```
1021 /// # use cedar_policy::{Authorizer,Context,Decision,Entities,EntityId,EntityTypeName, EntityUid, Request,PolicySet};
1022 /// # use std::str::FromStr;
1023 /// // create a request
1024 /// let p_eid = EntityId::from_str("alice").unwrap();
1025 /// let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1026 /// let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1027 ///
1028 /// let a_eid = EntityId::from_str("view").unwrap();
1029 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1030 /// let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1031 ///
1032 /// let r_eid = EntityId::from_str("trip").unwrap();
1033 /// let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1034 /// let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1035 ///
1036 /// let c = Context::empty();
1037 ///
1038 /// let request: Request = Request::new(p, a, r, c, None).unwrap();
1039 ///
1040 /// // create a policy
1041 /// let s = r#"
1042 /// permit (
1043 /// principal == User::"alice",
1044 /// action == Action::"view",
1045 /// resource == Album::"trip"
1046 /// )
1047 /// when { principal.ip_addr.isIpv4() };
1048 /// "#;
1049 /// let policy = PolicySet::from_str(s).expect("policy error");
1050 ///
1051 /// // create entities
1052 /// let e = r#"[
1053 /// {
1054 /// "uid": {"type":"User","id":"alice"},
1055 /// "attrs": {
1056 /// "age":19,
1057 /// "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1058 /// },
1059 /// "parents": []
1060 /// }
1061 /// ]"#;
1062 /// let entities = Entities::from_json_str(e, None).expect("entity error");
1063 ///
1064 /// let authorizer = Authorizer::new();
1065 /// let response = authorizer.is_authorized(&request, &policy, &entities);
1066 /// assert_eq!(response.decision(), Decision::Allow);
1067 /// ```
1068 pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
1069 self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
1070 }
1071
1072 /// A partially evaluated authorization request.
1073 /// The Authorizer will attempt to make as much progress as possible in the presence of unknowns.
1074 /// If the Authorizer can reach a response, it will return that response.
1075 /// Otherwise, it will return a list of residual policies that still need to be evaluated.
1076 #[doc = include_str!("../experimental_warning.md")]
1077 #[cfg(feature = "partial-eval")]
1078 pub fn is_authorized_partial(
1079 &self,
1080 query: &Request,
1081 policy_set: &PolicySet,
1082 entities: &Entities,
1083 ) -> PartialResponse {
1084 let response = self
1085 .0
1086 .is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
1087 PartialResponse(response)
1088 }
1089}
1090
1091/// Authorization response returned from the `Authorizer`
1092#[derive(Debug, PartialEq, Eq, Clone)]
1093pub struct Response {
1094 /// Authorization decision
1095 pub(crate) decision: Decision,
1096 /// Diagnostics providing more information on how this decision was reached
1097 pub(crate) diagnostics: Diagnostics,
1098}
1099
1100/// A partially evaluated authorization response.
1101///
1102/// Splits the results into several categories: satisfied, false, and residual for each policy effect.
1103/// Also tracks all the errors that were encountered during evaluation.
1104#[doc = include_str!("../experimental_warning.md")]
1105#[cfg(feature = "partial-eval")]
1106#[repr(transparent)]
1107#[derive(Debug, Clone, RefCast)]
1108pub struct PartialResponse(cedar_policy_core::authorizer::PartialResponse);
1109
1110#[cfg(feature = "partial-eval")]
1111impl PartialResponse {
1112 /// Attempt to reach a partial decision; the presence of residuals may result in returning [`None`],
1113 /// indicating that a decision could not be reached given the unknowns
1114 pub fn decision(&self) -> Option<Decision> {
1115 self.0.decision()
1116 }
1117
1118 /// Convert this response into a concrete evaluation response.
1119 /// All residuals are treated as errors
1120 pub fn concretize(self) -> Response {
1121 self.0.concretize().into()
1122 }
1123
1124 /// Returns the set of [`Policy`]s that were definitely satisfied.
1125 /// This will be the set of policies (both `permit` and `forbid`) that evaluated to `true`
1126 pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
1127 self.0.definitely_satisfied().map(Policy::from_ast)
1128 }
1129
1130 /// Returns the set of [`PolicyId`]s that encountered errors
1131 pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyId> {
1132 self.0.definitely_errored().map(PolicyId::ref_cast)
1133 }
1134
1135 /// Returns an over-approximation of the set of determining policies
1136 ///
1137 /// This is all policies that may be determining for any substitution of the unknowns.
1138 /// Policies not in this set will not affect the final decision, regardless of any
1139 /// substitutions.
1140 ///
1141 /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1142 pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1143 self.0.may_be_determining().map(Policy::from_ast)
1144 }
1145
1146 /// Returns an under-approximation of the set of determining policies
1147 ///
1148 /// This is all policies that must be determining for all possible substitutions of the unknowns.
1149 /// This set will include policies that evaluated to `true` and are guaranteed to be
1150 /// contributing to the final authorization decision.
1151 ///
1152 /// For more information on what counts as "determining" see: <https://docs.cedarpolicy.com/auth/authorization.html#request-authorization>
1153 pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
1154 self.0.must_be_determining().map(Policy::from_ast)
1155 }
1156
1157 /// Returns the set of non-trivial (meaning more than just `true` or `false`) residuals expressions
1158 pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1159 self.0.nontrivial_residuals().map(Policy::from_ast)
1160 }
1161
1162 /// Returns every policy as a residual expression
1163 pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
1164 self.0.all_residuals().map(Policy::from_ast)
1165 }
1166
1167 /// Returns all unknown entities during the evaluation of the response
1168 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
1169 let mut entity_uids = HashSet::new();
1170 for policy in self.0.all_residuals() {
1171 entity_uids.extend(policy.unknown_entities().into_iter().map(Into::into));
1172 }
1173 entity_uids
1174 }
1175
1176 /// Return the residual for a given [`PolicyId`], if it exists in the response
1177 pub fn get(&self, id: &PolicyId) -> Option<Policy> {
1178 self.0.get(id.as_ref()).map(Policy::from_ast)
1179 }
1180
1181 /// Attempt to re-authorize this response given a mapping from unknowns to values.
1182 #[allow(clippy::needless_pass_by_value)]
1183 #[deprecated = "use reauthorize_with_bindings"]
1184 pub fn reauthorize(
1185 &self,
1186 mapping: HashMap<SmolStr, RestrictedExpression>,
1187 auth: &Authorizer,
1188 es: &Entities,
1189 ) -> Result<Self, ReauthorizationError> {
1190 self.reauthorize_with_bindings(mapping.iter().map(|(k, v)| (k.as_str(), v)), auth, es)
1191 }
1192
1193 /// Attempt to re-authorize this response given a mapping from unknowns to values, provided as an iterator.
1194 /// Exhausts the iterator, returning any evaluation errors in the restricted expressions, regardless whether there is a matching unknown.
1195 pub fn reauthorize_with_bindings<'m>(
1196 &self,
1197 mapping: impl IntoIterator<Item = (&'m str, &'m RestrictedExpression)>,
1198 auth: &Authorizer,
1199 es: &Entities,
1200 ) -> Result<Self, ReauthorizationError> {
1201 let exts = Extensions::all_available();
1202 let evaluator = RestrictedEvaluator::new(exts);
1203 let mapping = mapping
1204 .into_iter()
1205 .map(|(name, expr)| {
1206 evaluator
1207 .interpret(BorrowedRestrictedExpr::new_unchecked(expr.0.as_ref()))
1208 .map(|v| (name.into(), v))
1209 })
1210 .collect::<Result<HashMap<_, _>, EvaluationError>>()?;
1211 let r = self.0.reauthorize(&mapping, &auth.0, &es.0)?;
1212 Ok(Self(r))
1213 }
1214}
1215
1216#[cfg(feature = "partial-eval")]
1217#[doc(hidden)]
1218impl From<cedar_policy_core::authorizer::PartialResponse> for PartialResponse {
1219 fn from(pr: cedar_policy_core::authorizer::PartialResponse) -> Self {
1220 Self(pr)
1221 }
1222}
1223
1224/// Diagnostics providing more information on how a `Decision` was reached
1225#[derive(Debug, PartialEq, Eq, Clone)]
1226pub struct Diagnostics {
1227 /// `PolicyId`s of the policies that contributed to the decision.
1228 /// If no policies applied to the request, this set will be empty.
1229 reason: HashSet<PolicyId>,
1230 /// Errors that occurred during authorization. The errors should be
1231 /// treated as unordered, since policies may be evaluated in any order.
1232 errors: Vec<AuthorizationError>,
1233}
1234
1235#[doc(hidden)]
1236impl From<authorizer::Diagnostics> for Diagnostics {
1237 fn from(diagnostics: authorizer::Diagnostics) -> Self {
1238 Self {
1239 reason: diagnostics.reason.into_iter().map(PolicyId::new).collect(),
1240 errors: diagnostics.errors.into_iter().map(Into::into).collect(),
1241 }
1242 }
1243}
1244
1245impl Diagnostics {
1246 /// Get the `PolicyId`s of the policies that contributed to the decision.
1247 /// If no policies applied to the request, this set will be empty.
1248 /// ```
1249 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1250 /// # EntityUid, Request,PolicySet};
1251 /// # use std::str::FromStr;
1252 /// # // create a request
1253 /// # let p_eid = EntityId::from_str("alice").unwrap();
1254 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1255 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1256 /// #
1257 /// # let a_eid = EntityId::from_str("view").unwrap();
1258 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1259 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1260 /// #
1261 /// # let r_eid = EntityId::from_str("trip").unwrap();
1262 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1263 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1264 /// #
1265 /// # let c = Context::empty();
1266 /// #
1267 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1268 /// #
1269 /// # // create a policy
1270 /// # let s = r#"permit(
1271 /// # principal == User::"alice",
1272 /// # action == Action::"view",
1273 /// # resource == Album::"trip"
1274 /// # )when{
1275 /// # principal.ip_addr.isIpv4()
1276 /// # };
1277 /// # "#;
1278 /// # let policy = PolicySet::from_str(s).expect("policy error");
1279 /// # // create entities
1280 /// # let e = r#"[
1281 /// # {
1282 /// # "uid": {"type":"User","id":"alice"},
1283 /// # "attrs": {
1284 /// # "age":19,
1285 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1286 /// # },
1287 /// # "parents": []
1288 /// # }
1289 /// # ]"#;
1290 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1291 /// let authorizer = Authorizer::new();
1292 /// let response = authorizer.is_authorized(&request, &policy, &entities);
1293 /// match response.decision() {
1294 /// Decision::Allow => println!("ALLOW"),
1295 /// Decision::Deny => println!("DENY"),
1296 /// }
1297 /// println!("note: this decision was due to the following policies:");
1298 /// for reason in response.diagnostics().reason() {
1299 /// println!("{}", reason);
1300 /// }
1301 /// ```
1302 pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
1303 self.reason.iter()
1304 }
1305
1306 /// Get the errors that occurred during authorization. The errors should be
1307 /// treated as unordered, since policies may be evaluated in any order.
1308 /// ```
1309 /// # use cedar_policy::{Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
1310 /// # EntityUid, Request,PolicySet};
1311 /// # use std::str::FromStr;
1312 /// # // create a request
1313 /// # let p_eid = EntityId::from_str("alice").unwrap();
1314 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
1315 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
1316 /// #
1317 /// # let a_eid = EntityId::from_str("view").unwrap();
1318 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
1319 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
1320 /// #
1321 /// # let r_eid = EntityId::from_str("trip").unwrap();
1322 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
1323 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
1324 /// #
1325 /// # let c = Context::empty();
1326 /// #
1327 /// # let request: Request = Request::new(p, a, r, c, None).unwrap();
1328 /// #
1329 /// # // create a policy
1330 /// # let s = r#"permit(
1331 /// # principal == User::"alice",
1332 /// # action == Action::"view",
1333 /// # resource == Album::"trip"
1334 /// # )when{
1335 /// # principal.ip_addr.isIpv4()
1336 /// # };
1337 /// # "#;
1338 /// # let policy = PolicySet::from_str(s).expect("policy error");
1339 /// # // create entities
1340 /// # let e = r#"[
1341 /// # {
1342 /// # "uid": {"type":"User","id":"alice"},
1343 /// # "attrs": {
1344 /// # "age":19,
1345 /// # "ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
1346 /// # },
1347 /// # "parents": []
1348 /// # }
1349 /// # ]"#;
1350 /// # let entities = Entities::from_json_str(e, None).expect("entity error");
1351 /// let authorizer = Authorizer::new();
1352 /// let response = authorizer.is_authorized(&request, &policy, &entities);
1353 /// match response.decision() {
1354 /// Decision::Allow => println!("ALLOW"),
1355 /// Decision::Deny => println!("DENY"),
1356 /// }
1357 /// for err in response.diagnostics().errors() {
1358 /// println!("{}", err);
1359 /// }
1360 /// ```
1361 pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
1362 self.errors.iter()
1363 }
1364
1365 /// Consume the `Diagnostics`, producing owned versions of `reason()` and `errors()`
1366 pub(crate) fn into_components(
1367 self,
1368 ) -> (
1369 impl Iterator<Item = PolicyId>,
1370 impl Iterator<Item = AuthorizationError>,
1371 ) {
1372 (self.reason.into_iter(), self.errors.into_iter())
1373 }
1374}
1375
1376impl Response {
1377 /// Create a new `Response`
1378 pub fn new(
1379 decision: Decision,
1380 reason: HashSet<PolicyId>,
1381 errors: Vec<AuthorizationError>,
1382 ) -> Self {
1383 Self {
1384 decision,
1385 diagnostics: Diagnostics { reason, errors },
1386 }
1387 }
1388
1389 /// Get the authorization decision
1390 pub fn decision(&self) -> Decision {
1391 self.decision
1392 }
1393
1394 /// Get the authorization diagnostics
1395 pub fn diagnostics(&self) -> &Diagnostics {
1396 &self.diagnostics
1397 }
1398}
1399
1400#[doc(hidden)]
1401impl From<authorizer::Response> for Response {
1402 fn from(a: authorizer::Response) -> Self {
1403 Self {
1404 decision: a.decision,
1405 diagnostics: a.diagnostics.into(),
1406 }
1407 }
1408}
1409
1410/// Used to select how a policy will be validated.
1411#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
1412#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
1413#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
1414#[serde(rename_all = "camelCase")]
1415#[non_exhaustive]
1416pub enum ValidationMode {
1417 /// Validate that policies do not contain any type errors, and additionally
1418 /// have a restricted form which is amenable for analysis.
1419 #[default]
1420 Strict,
1421 /// Validate that policies do not contain any type errors.
1422 #[doc = include_str!("../experimental_warning.md")]
1423 #[cfg(feature = "permissive-validate")]
1424 Permissive,
1425 /// Validate using a partial schema. Policies may contain type errors.
1426 #[doc = include_str!("../experimental_warning.md")]
1427 #[cfg(feature = "partial-validate")]
1428 Partial,
1429}
1430
1431#[doc(hidden)]
1432impl From<ValidationMode> for cedar_policy_core::validator::ValidationMode {
1433 fn from(mode: ValidationMode) -> Self {
1434 match mode {
1435 ValidationMode::Strict => Self::Strict,
1436 #[cfg(feature = "permissive-validate")]
1437 ValidationMode::Permissive => Self::Permissive,
1438 #[cfg(feature = "partial-validate")]
1439 ValidationMode::Partial => Self::Partial,
1440 }
1441 }
1442}
1443
1444/// Validator object, which provides policy validation and typechecking.
1445#[repr(transparent)]
1446#[derive(Debug, Clone, RefCast)]
1447pub struct Validator(cedar_policy_core::validator::Validator);
1448
1449#[doc(hidden)] // because this converts to a private/internal type
1450impl AsRef<cedar_policy_core::validator::Validator> for Validator {
1451 fn as_ref(&self) -> &cedar_policy_core::validator::Validator {
1452 &self.0
1453 }
1454}
1455
1456impl Validator {
1457 /// Construct a new `Validator` to validate policies using the given
1458 /// `Schema`.
1459 pub fn new(schema: Schema) -> Self {
1460 Self(cedar_policy_core::validator::Validator::new(schema.0))
1461 }
1462
1463 /// Get the `Schema` this `Validator` is using.
1464 pub fn schema(&self) -> &Schema {
1465 RefCast::ref_cast(self.0.schema())
1466 }
1467
1468 /// Validate all policies in a policy set, collecting all validation errors
1469 /// found into the returned `ValidationResult`. Each error is returned together with the
1470 /// policy id of the policy where the error was found. If a policy id
1471 /// included in the input policy set does not appear in the output iterator, then
1472 /// that policy passed the validator. If the function `validation_passed`
1473 /// returns true, then there were no validation errors found, so all
1474 /// policies in the policy set have passed the validator.
1475 pub fn validate(&self, pset: &PolicySet, mode: ValidationMode) -> ValidationResult {
1476 ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
1477 }
1478
1479 /// Validate all policies in a policy set, collecting all validation errors
1480 /// found into the returned `ValidationResult`. If validation passes, run level
1481 /// validation (RFC 76). Each error is returned together with the policy id of the policy
1482 /// where the error was found. If a policy id included in the input policy set does not
1483 /// appear in the output iterator, then that policy passed the validator. If the function
1484 /// `validation_passed` returns true, then there were no validation errors found, so
1485 /// all policies in the policy set have passed the validator.
1486 pub fn validate_with_level(
1487 &self,
1488 pset: &PolicySet,
1489 mode: ValidationMode,
1490 max_deref_level: u32,
1491 ) -> ValidationResult {
1492 ValidationResult::from(
1493 self.0
1494 .validate_with_level(&pset.ast, mode.into(), max_deref_level),
1495 )
1496 }
1497}
1498
1499/// Contains all the type information used to construct a `Schema` that can be
1500/// used to validate a policy.
1501#[derive(Debug, Clone)]
1502pub struct SchemaFragment {
1503 value: cedar_policy_core::validator::ValidatorSchemaFragment<
1504 cedar_policy_core::validator::ConditionalName,
1505 cedar_policy_core::validator::ConditionalName,
1506 >,
1507 lossless:
1508 cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1509}
1510
1511#[doc(hidden)] // because this converts to a private/internal type
1512impl
1513 AsRef<
1514 cedar_policy_core::validator::ValidatorSchemaFragment<
1515 cedar_policy_core::validator::ConditionalName,
1516 cedar_policy_core::validator::ConditionalName,
1517 >,
1518 > for SchemaFragment
1519{
1520 fn as_ref(
1521 &self,
1522 ) -> &cedar_policy_core::validator::ValidatorSchemaFragment<
1523 cedar_policy_core::validator::ConditionalName,
1524 cedar_policy_core::validator::ConditionalName,
1525 > {
1526 &self.value
1527 }
1528}
1529
1530#[doc(hidden)] // because this converts from a private/internal type
1531impl
1532 TryFrom<
1533 cedar_policy_core::validator::json_schema::Fragment<cedar_policy_core::validator::RawName>,
1534 > for SchemaFragment
1535{
1536 type Error = SchemaError;
1537 fn try_from(
1538 json_frag: cedar_policy_core::validator::json_schema::Fragment<
1539 cedar_policy_core::validator::RawName,
1540 >,
1541 ) -> Result<Self, Self::Error> {
1542 Ok(Self {
1543 value: json_frag.clone().try_into()?,
1544 lossless: json_frag,
1545 })
1546 }
1547}
1548
1549fn get_annotation_by_key(
1550 annotations: &est::Annotations,
1551 annotation_key: impl AsRef<str>,
1552) -> Option<&str> {
1553 annotations
1554 .0
1555 .get(&annotation_key.as_ref().parse().ok()?)
1556 .map(|value| annotation_value_to_str_ref(value.as_ref()))
1557}
1558
1559fn annotation_value_to_str_ref(value: Option<&ast::Annotation>) -> &str {
1560 value.map_or("", |a| a.as_ref())
1561}
1562
1563fn annotations_to_pairs(annotations: &est::Annotations) -> impl Iterator<Item = (&str, &str)> {
1564 annotations
1565 .0
1566 .iter()
1567 .map(|(key, value)| (key.as_ref(), annotation_value_to_str_ref(value.as_ref())))
1568}
1569
1570impl SchemaFragment {
1571 /// Get annotations of a non-empty namespace.
1572 ///
1573 /// We do not allow namespace-level annotations on the empty namespace.
1574 ///
1575 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1576 pub fn namespace_annotations(
1577 &self,
1578 namespace: EntityNamespace,
1579 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1580 self.lossless
1581 .0
1582 .get(&Some(namespace.0))
1583 .map(|ns_def| annotations_to_pairs(&ns_def.annotations))
1584 }
1585
1586 /// Get annotation value of a non-empty namespace by annotation key
1587 /// `annotation_key`
1588 ///
1589 /// We do not allow namespace-level annotations on the empty namespace.
1590 ///
1591 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1592 /// or `annotation_key` is not a valid annotation key
1593 /// or it does not exist
1594 pub fn namespace_annotation(
1595 &self,
1596 namespace: EntityNamespace,
1597 annotation_key: impl AsRef<str>,
1598 ) -> Option<&str> {
1599 let ns = self.lossless.0.get(&Some(namespace.0))?;
1600 get_annotation_by_key(&ns.annotations, annotation_key)
1601 }
1602
1603 /// Get annotations of a common type declaration
1604 ///
1605 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1606 /// `ty` is not a valid common type ID or `ty` is not found in the
1607 /// corresponding namespace definition
1608 pub fn common_type_annotations(
1609 &self,
1610 namespace: Option<EntityNamespace>,
1611 ty: &str,
1612 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1613 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1614 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1615 .ok()?;
1616 ns_def
1617 .common_types
1618 .get(&ty)
1619 .map(|ty| annotations_to_pairs(&ty.annotations))
1620 }
1621
1622 /// Get annotation value of a common type declaration by annotation key
1623 /// `annotation_key`
1624 ///
1625 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1626 /// or `ty` is not a valid common type ID
1627 /// or `ty` is not found in the corresponding namespace definition
1628 /// or `annotation_key` is not a valid annotation key
1629 /// or it does not exist
1630 pub fn common_type_annotation(
1631 &self,
1632 namespace: Option<EntityNamespace>,
1633 ty: &str,
1634 annotation_key: impl AsRef<str>,
1635 ) -> Option<&str> {
1636 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1637 let ty = json_schema::CommonTypeId::new(ast::UnreservedId::from_normalized_str(ty).ok()?)
1638 .ok()?;
1639 get_annotation_by_key(&ns_def.common_types.get(&ty)?.annotations, annotation_key)
1640 }
1641
1642 /// Get annotations of an entity type declaration
1643 ///
1644 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1645 /// `ty` is not a valid entity type name or `ty` is not found in the
1646 /// corresponding namespace definition
1647 pub fn entity_type_annotations(
1648 &self,
1649 namespace: Option<EntityNamespace>,
1650 ty: &str,
1651 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1652 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1653 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1654 ns_def
1655 .entity_types
1656 .get(&ty)
1657 .map(|ty| annotations_to_pairs(&ty.annotations))
1658 }
1659
1660 /// Get annotation value of an entity type declaration by annotation key
1661 /// `annotation_key`
1662 ///
1663 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1664 /// or `ty` is not a valid entity type name
1665 /// or `ty` is not found in the corresponding namespace definition
1666 /// or `annotation_key` is not a valid annotation key
1667 /// or it does not exist
1668 pub fn entity_type_annotation(
1669 &self,
1670 namespace: Option<EntityNamespace>,
1671 ty: &str,
1672 annotation_key: impl AsRef<str>,
1673 ) -> Option<&str> {
1674 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1675 let ty = ast::UnreservedId::from_normalized_str(ty).ok()?;
1676 get_annotation_by_key(&ns_def.entity_types.get(&ty)?.annotations, annotation_key)
1677 }
1678
1679 /// Get annotations of an action declaration
1680 ///
1681 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`] or
1682 /// `id` is not found in the corresponding namespace definition
1683 pub fn action_annotations(
1684 &self,
1685 namespace: Option<EntityNamespace>,
1686 id: &EntityId,
1687 ) -> Option<impl Iterator<Item = (&str, &str)>> {
1688 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1689 ns_def
1690 .actions
1691 .get(id.unescaped())
1692 .map(|a| annotations_to_pairs(&a.annotations))
1693 }
1694
1695 /// Get annotation value of an action declaration by annotation key
1696 /// `annotation_key`
1697 ///
1698 /// Returns `None` if `namespace` is not found in the [`SchemaFragment`]
1699 /// or `id` is not found in the corresponding namespace definition
1700 /// or `annotation_key` is not a valid annotation key
1701 /// or it does not exist
1702 pub fn action_annotation(
1703 &self,
1704 namespace: Option<EntityNamespace>,
1705 id: &EntityId,
1706 annotation_key: impl AsRef<str>,
1707 ) -> Option<&str> {
1708 let ns_def = self.lossless.0.get(&namespace.map(|n| n.0))?;
1709 get_annotation_by_key(
1710 &ns_def.actions.get(id.unescaped())?.annotations,
1711 annotation_key,
1712 )
1713 }
1714
1715 /// Extract namespaces defined in this [`SchemaFragment`].
1716 ///
1717 /// `None` indicates the empty namespace.
1718 pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
1719 self.value.namespaces().filter_map(|ns| {
1720 match ns.map(|ns| ast::Name::try_from(ns.clone())) {
1721 Some(Ok(n)) => Some(Some(EntityNamespace(n))),
1722 None => Some(None), // empty namespace, which we want to surface to the user
1723 Some(Err(_)) => {
1724 // if the `SchemaFragment` contains namespaces with
1725 // reserved `__cedar` components, that's an internal
1726 // implementation detail; hide that from the user.
1727 // Also note that `EntityNamespace` is backed by `Name`
1728 // which can't even contain names with reserved
1729 // `__cedar` components.
1730 None
1731 }
1732 }
1733 })
1734 }
1735
1736 /// Create a [`SchemaFragment`] from a string containing JSON in the
1737 /// JSON schema format.
1738 pub fn from_json_str(src: &str) -> Result<Self, SchemaError> {
1739 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_str(src)?;
1740 Ok(Self {
1741 value: lossless.clone().try_into()?,
1742 lossless,
1743 })
1744 }
1745
1746 /// Create a [`SchemaFragment`] from a JSON value (which should be an
1747 /// object of the shape required for the JSON schema format).
1748 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1749 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_value(json)?;
1750 Ok(Self {
1751 value: lossless.clone().try_into()?,
1752 lossless,
1753 })
1754 }
1755
1756 /// Parse a [`SchemaFragment`] from a reader containing the Cedar schema syntax
1757 pub fn from_cedarschema_file(
1758 r: impl std::io::Read,
1759 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1760 let (lossless, warnings) =
1761 cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_file(
1762 r,
1763 Extensions::all_available(),
1764 )?;
1765 Ok((
1766 Self {
1767 value: lossless.clone().try_into()?,
1768 lossless,
1769 },
1770 warnings,
1771 ))
1772 }
1773
1774 /// Parse a [`SchemaFragment`] from a string containing the Cedar schema syntax
1775 pub fn from_cedarschema_str(
1776 src: &str,
1777 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1778 let (lossless, warnings) =
1779 cedar_policy_core::validator::json_schema::Fragment::from_cedarschema_str(
1780 src,
1781 Extensions::all_available(),
1782 )?;
1783 Ok((
1784 Self {
1785 value: lossless.clone().try_into()?,
1786 lossless,
1787 },
1788 warnings,
1789 ))
1790 }
1791
1792 /// Create a [`SchemaFragment`] directly from a JSON file (which should
1793 /// contain an object of the shape required for the JSON schema format).
1794 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1795 let lossless = cedar_policy_core::validator::json_schema::Fragment::from_json_file(file)?;
1796 Ok(Self {
1797 value: lossless.clone().try_into()?,
1798 lossless,
1799 })
1800 }
1801
1802 /// Serialize this [`SchemaFragment`] as a JSON value
1803 pub fn to_json_value(self) -> Result<serde_json::Value, SchemaError> {
1804 serde_json::to_value(self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1805 }
1806
1807 /// Serialize this [`SchemaFragment`] as a JSON string
1808 pub fn to_json_string(&self) -> Result<String, SchemaError> {
1809 serde_json::to_string(&self.lossless).map_err(|e| SchemaError::JsonSerialization(e.into()))
1810 }
1811
1812 /// Serialize this [`SchemaFragment`] into a string in the Cedar schema
1813 /// syntax
1814 pub fn to_cedarschema(&self) -> Result<String, ToCedarSchemaError> {
1815 let str = self.lossless.to_cedarschema()?;
1816 Ok(str)
1817 }
1818}
1819
1820impl TryInto<Schema> for SchemaFragment {
1821 type Error = SchemaError;
1822
1823 /// Convert [`SchemaFragment`] into a [`Schema`]. To build the [`Schema`] we
1824 /// need to have all entity types defined, so an error will be returned if
1825 /// any undeclared entity types are referenced in the schema fragment.
1826 fn try_into(self) -> Result<Schema, Self::Error> {
1827 Ok(Schema(
1828 cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1829 [self.value],
1830 Extensions::all_available(),
1831 )?,
1832 ))
1833 }
1834}
1835
1836impl FromStr for SchemaFragment {
1837 type Err = CedarSchemaError;
1838 /// Construct [`SchemaFragment`] from a string containing a schema formatted
1839 /// in the Cedar schema format. This can fail if the string is not a valid
1840 /// schema. This function does not check for consistency in the schema
1841 /// (e.g., references to undefined entities) because this is not required
1842 /// until a `Schema` is constructed.
1843 fn from_str(src: &str) -> Result<Self, Self::Err> {
1844 Self::from_cedarschema_str(src).map(|(frag, _)| frag)
1845 }
1846}
1847
1848/// Object containing schema information used by the validator.
1849#[repr(transparent)]
1850#[derive(Debug, Clone, RefCast)]
1851pub struct Schema(pub(crate) cedar_policy_core::validator::ValidatorSchema);
1852
1853#[doc(hidden)] // because this converts to a private/internal type
1854impl AsRef<cedar_policy_core::validator::ValidatorSchema> for Schema {
1855 fn as_ref(&self) -> &cedar_policy_core::validator::ValidatorSchema {
1856 &self.0
1857 }
1858}
1859
1860#[doc(hidden)]
1861impl From<cedar_policy_core::validator::ValidatorSchema> for Schema {
1862 fn from(schema: cedar_policy_core::validator::ValidatorSchema) -> Self {
1863 Self(schema)
1864 }
1865}
1866
1867impl FromStr for Schema {
1868 type Err = CedarSchemaError;
1869
1870 /// Construct a [`Schema`] from a string containing a schema formatted in
1871 /// the Cedar schema format. This can fail if it is not possible to parse a
1872 /// schema from the string, or if errors in values in the schema are
1873 /// uncovered after parsing. For instance, when an entity attribute name is
1874 /// found to not be a valid attribute name according to the Cedar
1875 /// grammar.
1876 fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
1877 Self::from_cedarschema_str(schema_src).map(|(schema, _)| schema)
1878 }
1879}
1880
1881impl Schema {
1882 /// Create a [`Schema`] from multiple [`SchemaFragment`]. The individual
1883 /// fragments may reference entity or common types that are not declared in that
1884 /// fragment, but all referenced entity and common types must be declared in some
1885 /// fragment.
1886 pub fn from_schema_fragments(
1887 fragments: impl IntoIterator<Item = SchemaFragment>,
1888 ) -> Result<Self, SchemaError> {
1889 Ok(Self(
1890 cedar_policy_core::validator::ValidatorSchema::from_schema_fragments(
1891 fragments.into_iter().map(|f| f.value),
1892 Extensions::all_available(),
1893 )?,
1894 ))
1895 }
1896
1897 /// Create a [`Schema`] from a JSON value (which should be an object of the
1898 /// shape required for the JSON schema format).
1899 pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
1900 Ok(Self(
1901 cedar_policy_core::validator::ValidatorSchema::from_json_value(
1902 json,
1903 Extensions::all_available(),
1904 )?,
1905 ))
1906 }
1907
1908 /// Create a [`Schema`] from a string containing JSON in the appropriate
1909 /// shape.
1910 pub fn from_json_str(json: &str) -> Result<Self, SchemaError> {
1911 Ok(Self(
1912 cedar_policy_core::validator::ValidatorSchema::from_json_str(
1913 json,
1914 Extensions::all_available(),
1915 )?,
1916 ))
1917 }
1918
1919 /// Create a [`Schema`] directly from a file containing JSON in the
1920 /// appropriate shape.
1921 pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
1922 Ok(Self(
1923 cedar_policy_core::validator::ValidatorSchema::from_json_file(
1924 file,
1925 Extensions::all_available(),
1926 )?,
1927 ))
1928 }
1929
1930 /// Parse the schema from a reader, in the Cedar schema format.
1931 pub fn from_cedarschema_file(
1932 file: impl std::io::Read,
1933 ) -> Result<(Self, impl Iterator<Item = SchemaWarning> + 'static), CedarSchemaError> {
1934 let (schema, warnings) =
1935 cedar_policy_core::validator::ValidatorSchema::from_cedarschema_file(
1936 file,
1937 Extensions::all_available(),
1938 )?;
1939 Ok((Self(schema), warnings))
1940 }
1941
1942 /// Parse the schema from a string, in the Cedar schema format.
1943 pub fn from_cedarschema_str(
1944 src: &str,
1945 ) -> Result<(Self, impl Iterator<Item = SchemaWarning>), CedarSchemaError> {
1946 let (schema, warnings) =
1947 cedar_policy_core::validator::ValidatorSchema::from_cedarschema_str(
1948 src,
1949 Extensions::all_available(),
1950 )?;
1951 Ok((Self(schema), warnings))
1952 }
1953
1954 /// Extract from the schema an [`Entities`] containing the action entities
1955 /// declared in the schema.
1956 pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
1957 Ok(Entities(self.0.action_entities()?))
1958 }
1959
1960 /// Returns an iterator over every entity type that can be a principal for any action in this schema
1961 ///
1962 /// Note: this iterator may contain duplicates.
1963 ///
1964 /// # Examples
1965 /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of principals
1966 /// ```
1967 /// use std::collections::HashSet;
1968 /// use cedar_policy::Schema;
1969 /// let schema : Schema = r#"
1970 /// entity User;
1971 /// entity Folder;
1972 /// action Access appliesTo {
1973 /// principal : User,
1974 /// resource : Folder,
1975 /// };
1976 /// action Delete appliesTo {
1977 /// principal : User,
1978 /// resource : Folder,
1979 /// };
1980 /// "#.parse().unwrap();
1981 /// let principals = schema.principals().collect::<HashSet<_>>();
1982 /// assert_eq!(principals, HashSet::from([&"User".parse().unwrap()]));
1983 /// ```
1984 pub fn principals(&self) -> impl Iterator<Item = &EntityTypeName> {
1985 self.0.principals().map(RefCast::ref_cast)
1986 }
1987
1988 /// Returns an iterator over every entity type that can be a resource for any action in this schema
1989 ///
1990 /// Note: this iterator may contain duplicates.
1991 /// # Examples
1992 /// Here's an example of using a [`std::collections::HashSet`] to get a de-duplicated set of resources
1993 /// ```
1994 /// use std::collections::HashSet;
1995 /// use cedar_policy::Schema;
1996 /// let schema : Schema = r#"
1997 /// entity User;
1998 /// entity Folder;
1999 /// action Access appliesTo {
2000 /// principal : User,
2001 /// resource : Folder,
2002 /// };
2003 /// action Delete appliesTo {
2004 /// principal : User,
2005 /// resource : Folder,
2006 /// };
2007 /// "#.parse().unwrap();
2008 /// let resources = schema.resources().collect::<HashSet<_>>();
2009 /// assert_eq!(resources, HashSet::from([&"Folder".parse().unwrap()]));
2010 /// ```
2011 pub fn resources(&self) -> impl Iterator<Item = &EntityTypeName> {
2012 self.0.resources().map(RefCast::ref_cast)
2013 }
2014
2015 /// Returns an iterator over every entity type that can be a principal for `action` in this schema
2016 ///
2017 /// ## Errors
2018 ///
2019 /// Returns [`None`] if `action` is not found in the schema
2020 pub fn principals_for_action(
2021 &self,
2022 action: &EntityUid,
2023 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2024 self.0
2025 .principals_for_action(&action.0)
2026 .map(|iter| iter.map(RefCast::ref_cast))
2027 }
2028
2029 /// Returns an iterator over every entity type that can be a resource for `action` in this schema
2030 ///
2031 /// ## Errors
2032 ///
2033 /// Returns [`None`] if `action` is not found in the schema
2034 pub fn resources_for_action(
2035 &self,
2036 action: &EntityUid,
2037 ) -> Option<impl Iterator<Item = &EntityTypeName>> {
2038 self.0
2039 .resources_for_action(&action.0)
2040 .map(|iter| iter.map(RefCast::ref_cast))
2041 }
2042
2043 /// Returns an iterator over all the [`RequestEnv`]s that are valid
2044 /// according to this schema.
2045 pub fn request_envs(&self) -> impl Iterator<Item = RequestEnv> + '_ {
2046 self.0
2047 .unlinked_request_envs(cedar_policy_core::validator::ValidationMode::Strict)
2048 .map(Into::into)
2049 }
2050
2051 /// Returns an iterator over all the entity types that can be an ancestor of `ty`
2052 ///
2053 /// ## Errors
2054 ///
2055 /// Returns [`None`] if the `ty` is not found in the schema
2056 pub fn ancestors<'a>(
2057 &'a self,
2058 ty: &'a EntityTypeName,
2059 ) -> Option<impl Iterator<Item = &'a EntityTypeName> + 'a> {
2060 self.0
2061 .ancestors(&ty.0)
2062 .map(|iter| iter.map(RefCast::ref_cast))
2063 }
2064
2065 /// Returns an iterator over all the action groups defined in this schema
2066 pub fn action_groups(&self) -> impl Iterator<Item = &EntityUid> {
2067 self.0.action_groups().map(RefCast::ref_cast)
2068 }
2069
2070 /// Returns an iterator over all entity types defined in this schema
2071 pub fn entity_types(&self) -> impl Iterator<Item = &EntityTypeName> {
2072 self.0
2073 .entity_types()
2074 .map(|ety| RefCast::ref_cast(ety.name()))
2075 }
2076
2077 /// Returns an iterator over all actions defined in this schema
2078 pub fn actions(&self) -> impl Iterator<Item = &EntityUid> {
2079 self.0.actions().map(RefCast::ref_cast)
2080 }
2081
2082 /// Returns an iterator over the actions that apply to this principal and
2083 /// resource type, as specified by the `appliesTo` block for the action in
2084 /// this schema.
2085 pub fn actions_for_principal_and_resource<'a: 'b, 'b>(
2086 &'a self,
2087 principal_type: &'b EntityTypeName,
2088 resource_type: &'b EntityTypeName,
2089 ) -> impl Iterator<Item = &'a EntityUid> + 'b {
2090 self.0
2091 .actions_for_principal_and_resource(&principal_type.0, &resource_type.0)
2092 .map(RefCast::ref_cast)
2093 }
2094}
2095
2096/// Contains the result of policy validation.
2097///
2098/// The result includes the list of issues found by validation and whether validation succeeds or fails.
2099/// Validation succeeds if there are no fatal errors. There may still be
2100/// non-fatal warnings present when validation passes.
2101#[derive(Debug, Clone)]
2102pub struct ValidationResult {
2103 validation_errors: Vec<ValidationError>,
2104 validation_warnings: Vec<ValidationWarning>,
2105}
2106
2107impl ValidationResult {
2108 /// True when validation passes. There are no errors, but there may be
2109 /// non-fatal warnings. Use [`ValidationResult::validation_passed_without_warnings`]
2110 /// to check that there are also no warnings.
2111 pub fn validation_passed(&self) -> bool {
2112 self.validation_errors.is_empty()
2113 }
2114
2115 /// True when validation passes (i.e., there are no errors) and there are
2116 /// additionally no non-fatal warnings.
2117 pub fn validation_passed_without_warnings(&self) -> bool {
2118 self.validation_errors.is_empty() && self.validation_warnings.is_empty()
2119 }
2120
2121 /// Get an iterator over the errors found by the validator.
2122 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
2123 self.validation_errors.iter()
2124 }
2125
2126 /// Get an iterator over the warnings found by the validator.
2127 pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
2128 self.validation_warnings.iter()
2129 }
2130
2131 fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
2132 self.validation_errors
2133 .first()
2134 .map(|e| e as &dyn Diagnostic)
2135 .or_else(|| {
2136 self.validation_warnings
2137 .first()
2138 .map(|w| w as &dyn Diagnostic)
2139 })
2140 }
2141
2142 pub(crate) fn into_errors_and_warnings(
2143 self,
2144 ) -> (
2145 impl Iterator<Item = ValidationError>,
2146 impl Iterator<Item = ValidationWarning>,
2147 ) {
2148 (
2149 self.validation_errors.into_iter(),
2150 self.validation_warnings.into_iter(),
2151 )
2152 }
2153}
2154
2155#[doc(hidden)]
2156impl From<cedar_policy_core::validator::ValidationResult> for ValidationResult {
2157 fn from(r: cedar_policy_core::validator::ValidationResult) -> Self {
2158 let (errors, warnings) = r.into_errors_and_warnings();
2159 Self {
2160 validation_errors: errors.map(ValidationError::from).collect(),
2161 validation_warnings: warnings.map(ValidationWarning::from).collect(),
2162 }
2163 }
2164}
2165
2166impl std::fmt::Display for ValidationResult {
2167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2168 match self.first_error_or_warning() {
2169 Some(diagnostic) => write!(f, "{diagnostic}"),
2170 None => write!(f, "no errors or warnings"),
2171 }
2172 }
2173}
2174
2175impl std::error::Error for ValidationResult {
2176 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2177 self.first_error_or_warning()
2178 .and_then(std::error::Error::source)
2179 }
2180
2181 #[allow(deprecated)]
2182 fn description(&self) -> &str {
2183 self.first_error_or_warning()
2184 .map_or("no errors or warnings", std::error::Error::description)
2185 }
2186
2187 #[allow(deprecated)]
2188 fn cause(&self) -> Option<&dyn std::error::Error> {
2189 self.first_error_or_warning()
2190 .and_then(std::error::Error::cause)
2191 }
2192}
2193
2194// Except for `.related()`, and `.severity` everything is forwarded to the first
2195// error, or to the first warning if there are no errors. This is done for the
2196// same reason as policy parse errors.
2197impl Diagnostic for ValidationResult {
2198 fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
2199 let mut related = self
2200 .validation_errors
2201 .iter()
2202 .map(|err| err as &dyn Diagnostic)
2203 .chain(
2204 self.validation_warnings
2205 .iter()
2206 .map(|warn| warn as &dyn Diagnostic),
2207 );
2208 related.next().map(move |first| match first.related() {
2209 Some(first_related) => Box::new(first_related.chain(related)),
2210 None => Box::new(related) as Box<dyn Iterator<Item = _>>,
2211 })
2212 }
2213
2214 fn severity(&self) -> Option<miette::Severity> {
2215 self.first_error_or_warning()
2216 .map_or(Some(miette::Severity::Advice), Diagnostic::severity)
2217 }
2218
2219 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
2220 self.first_error_or_warning().and_then(Diagnostic::labels)
2221 }
2222
2223 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
2224 self.first_error_or_warning()
2225 .and_then(Diagnostic::source_code)
2226 }
2227
2228 fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2229 self.first_error_or_warning().and_then(Diagnostic::code)
2230 }
2231
2232 fn url(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2233 self.first_error_or_warning().and_then(Diagnostic::url)
2234 }
2235
2236 fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2237 self.first_error_or_warning().and_then(Diagnostic::help)
2238 }
2239
2240 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
2241 self.first_error_or_warning()
2242 .and_then(Diagnostic::diagnostic_source)
2243 }
2244}
2245
2246/// Scan a set of policies for potentially confusing/obfuscating text.
2247///
2248/// These checks are also provided through [`Validator::validate`] which provides more
2249/// comprehensive error detection, but this function can be used to check for
2250/// confusable strings without defining a schema.
2251pub fn confusable_string_checker<'a>(
2252 templates: impl Iterator<Item = &'a Template> + 'a,
2253) -> impl Iterator<Item = ValidationWarning> + 'a {
2254 cedar_policy_core::validator::confusable_string_checks(templates.map(|t| &t.ast))
2255 .map(std::convert::Into::into)
2256}
2257
2258/// Represents a namespace.
2259///
2260/// An `EntityNamespace` can can be constructed using
2261/// [`EntityNamespace::from_str`] or by calling `parse()` on a string.
2262/// _This can fail_, so it is important to properly handle an `Err` result.
2263///
2264/// ```
2265/// # use cedar_policy::EntityNamespace;
2266/// let id : Result<EntityNamespace, _> = "My::Name::Space".parse();
2267/// # assert_eq!(id.unwrap().to_string(), "My::Name::Space".to_string());
2268/// ```
2269#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
2270pub struct EntityNamespace(pub(crate) ast::Name);
2271
2272#[doc(hidden)] // because this converts to a private/internal type
2273impl AsRef<ast::Name> for EntityNamespace {
2274 fn as_ref(&self) -> &ast::Name {
2275 &self.0
2276 }
2277}
2278
2279/// This `FromStr` implementation requires the _normalized_ representation of the
2280/// namespace. See <https://github.com/cedar-policy/rfcs/pull/9/>.
2281impl FromStr for EntityNamespace {
2282 type Err = ParseErrors;
2283
2284 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
2285 ast::Name::from_normalized_str(namespace_str)
2286 .map(EntityNamespace)
2287 .map_err(Into::into)
2288 }
2289}
2290
2291impl std::fmt::Display for EntityNamespace {
2292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2293 write!(f, "{}", self.0)
2294 }
2295}
2296
2297#[derive(Debug, Clone, Default)]
2298/// A struct representing a `PolicySet` as a series of strings for ser/de.
2299/// A `PolicySet` that contains template-linked policies cannot be
2300/// represented as this struct.
2301pub(crate) struct StringifiedPolicySet {
2302 /// The static policies in the set
2303 pub policies: Vec<String>,
2304 /// The policy templates in the set
2305 pub policy_templates: Vec<String>,
2306}
2307
2308/// Represents a set of `Policy`s
2309#[derive(Debug, Clone, Default)]
2310pub struct PolicySet {
2311 /// AST representation. Technically partially redundant with the other fields.
2312 /// Internally, we ensure that the duplicated information remains consistent.
2313 pub(crate) ast: ast::PolicySet,
2314 /// Policies in the set (this includes both static policies and template linked-policies)
2315 policies: LinkedHashMap<PolicyId, Policy>,
2316 /// Templates in the set
2317 templates: LinkedHashMap<PolicyId, Template>,
2318}
2319
2320impl PartialEq for PolicySet {
2321 fn eq(&self, other: &Self) -> bool {
2322 // eq is based on just the `ast`
2323 self.ast.eq(&other.ast)
2324 }
2325}
2326impl Eq for PolicySet {}
2327
2328#[doc(hidden)] // because this converts to a private/internal type
2329impl AsRef<ast::PolicySet> for PolicySet {
2330 fn as_ref(&self) -> &ast::PolicySet {
2331 &self.ast
2332 }
2333}
2334
2335#[doc(hidden)]
2336impl TryFrom<ast::PolicySet> for PolicySet {
2337 type Error = PolicySetError;
2338 fn try_from(pset: ast::PolicySet) -> Result<Self, Self::Error> {
2339 Self::from_ast(pset)
2340 }
2341}
2342
2343impl FromStr for PolicySet {
2344 type Err = ParseErrors;
2345
2346 /// Create a policy set from multiple statements.
2347 ///
2348 /// Policy ids will default to "policy*" with numbers from 0.
2349 /// If you load more policies, do not use the default id, or there will be conflicts.
2350 ///
2351 /// See [`Policy`] for more.
2352 fn from_str(policies: &str) -> Result<Self, Self::Err> {
2353 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
2354 // PANIC SAFETY: By the invariant on `parse_policyset_and_also_return_policy_text(policies)`, every `PolicyId` in `pset.policies()` occurs as a key in `text`.
2355 #[allow(clippy::expect_used)]
2356 let policies = pset.policies().map(|p|
2357 (
2358 PolicyId::new(p.id().clone()),
2359 Policy { lossless: LosslessPolicy::policy_or_template_text(*texts.get(p.id()).expect("internal invariant violation: policy id exists in asts but not texts")), ast: p.clone() }
2360 )
2361 ).collect();
2362 // PANIC SAFETY: By the same invariant, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.
2363 #[allow(clippy::expect_used)]
2364 let templates = pset.templates().map(|t|
2365 (
2366 PolicyId::new(t.id().clone()),
2367 Template { lossless: LosslessPolicy::policy_or_template_text(*texts.get(t.id()).expect("internal invariant violation: template id exists in asts but not ests")), ast: t.clone() }
2368 )
2369 ).collect();
2370 Ok(Self {
2371 ast: pset,
2372 policies,
2373 templates,
2374 })
2375 }
2376}
2377
2378impl PolicySet {
2379 /// Build the policy set AST from the EST
2380 fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
2381 let ast: ast::PolicySet = est.clone().try_into()?;
2382 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.policies()` occurs in `est`
2383 #[allow(clippy::expect_used)]
2384 let policies = ast
2385 .policies()
2386 .map(|p| {
2387 (
2388 PolicyId::new(p.id().clone()),
2389 Policy {
2390 lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
2391 "internal invariant violation: policy id exists in asts but not ests",
2392 )),
2393 ast: p.clone(),
2394 },
2395 )
2396 })
2397 .collect();
2398 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.templates()` occurs in `est`
2399 #[allow(clippy::expect_used)]
2400 let templates = ast
2401 .templates()
2402 .map(|t| {
2403 (
2404 PolicyId::new(t.id().clone()),
2405 Template {
2406 lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
2407 "internal invariant violation: template id exists in asts but not ests",
2408 )),
2409 ast: t.clone(),
2410 },
2411 )
2412 })
2413 .collect();
2414 Ok(Self {
2415 ast,
2416 policies,
2417 templates,
2418 })
2419 }
2420
2421 /// Build the [`PolicySet`] from just the AST information
2422 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
2423 pub(crate) fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
2424 Self::from_policies(ast.into_policies().map(Policy::from_ast))
2425 }
2426
2427 /// Deserialize the [`PolicySet`] from a JSON string
2428 pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
2429 let est: est::PolicySet = serde_json::from_str(src.as_ref())
2430 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2431 Self::from_est(&est)
2432 }
2433
2434 /// Deserialize the [`PolicySet`] from a JSON value
2435 pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
2436 let est: est::PolicySet = serde_json::from_value(src)
2437 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2438 Self::from_est(&est)
2439 }
2440
2441 /// Deserialize the [`PolicySet`] from a JSON reader
2442 pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
2443 let est: est::PolicySet = serde_json::from_reader(r)
2444 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2445 Self::from_est(&est)
2446 }
2447
2448 /// Serialize the [`PolicySet`] as a JSON value
2449 pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
2450 let est = self.est()?;
2451 let value = serde_json::to_value(est)
2452 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2453 Ok(value)
2454 }
2455
2456 /// Get the EST representation of the [`PolicySet`]
2457 fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
2458 let (static_policies, template_links): (Vec<_>, Vec<_>) =
2459 fold_partition(self.policies, is_static_or_link)?;
2460 let static_policies = static_policies.into_iter().collect::<LinkedHashMap<_, _>>();
2461 let templates = self
2462 .templates
2463 .into_iter()
2464 .map(|(id, template)| {
2465 template
2466 .lossless
2467 .est(|| template.ast.clone().into())
2468 .map(|est| (id.into(), est))
2469 })
2470 .collect::<Result<LinkedHashMap<_, _>, _>>()?;
2471 let est = est::PolicySet {
2472 templates,
2473 static_policies,
2474 template_links,
2475 };
2476
2477 Ok(est)
2478 }
2479
2480 /// Get the human-readable Cedar syntax representation of this policy set.
2481 /// This function is primarily intended for rendering JSON policies in the
2482 /// human-readable syntax, but it will also return the original policy text
2483 /// (though possibly re-ordering policies within the policy set) when the
2484 /// policy-set contains policies parsed from the human-readable syntax.
2485 ///
2486 /// This will return `None` if there are any linked policies in the policy
2487 /// set because they cannot be directly rendered in Cedar syntax. It also
2488 /// cannot record policy ids because these cannot be specified in the Cedar
2489 /// syntax. The policies may be reordered, so parsing the resulting string
2490 /// with [`PolicySet::from_str`] is likely to yield different policy id
2491 /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2492 /// only using this function to obtain a representation to display to human
2493 /// users.
2494 ///
2495 /// This function does not format the policy according to any particular
2496 /// rules. Policy formatting can be done through the Cedar policy CLI or
2497 /// the `cedar-policy-formatter` crate.
2498 pub fn to_cedar(&self) -> Option<String> {
2499 match self.stringify() {
2500 Some(StringifiedPolicySet {
2501 policies,
2502 policy_templates,
2503 }) => {
2504 let policies_as_vec = policies
2505 .into_iter()
2506 .chain(policy_templates)
2507 .collect::<Vec<_>>();
2508 Some(policies_as_vec.join("\n\n"))
2509 }
2510 None => None,
2511 }
2512 }
2513
2514 /// Get the human-readable Cedar syntax representation of this policy set,
2515 /// as a vec of strings. This function is useful to break up a large cedar
2516 /// file containing many policies into individual policies.
2517 ///
2518 /// This will return `None` if there are any linked policies in the policy
2519 /// set because they cannot be directly rendered in Cedar syntax. It also
2520 /// cannot record policy ids because these cannot be specified in the Cedar
2521 /// syntax. The policies may be reordered, so parsing the resulting string
2522 /// with [`PolicySet::from_str`] is likely to yield different policy id
2523 /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2524 /// only using this function to obtain a compact cedar representation,
2525 /// perhaps for storage purposes.
2526 ///
2527 /// This function does not format the policy according to any particular
2528 /// rules. Policy formatting can be done through the Cedar policy CLI or
2529 /// the `cedar-policy-formatter` crate.
2530 pub(crate) fn stringify(&self) -> Option<StringifiedPolicySet> {
2531 let policies = self
2532 .policies
2533 .values()
2534 // We'd like to print policies in a deterministic order, so we sort
2535 // before printing, hoping that the size of policy sets is fairly
2536 // small.
2537 .sorted_by_key(|p| AsRef::<str>::as_ref(p.id()))
2538 .map(Policy::to_cedar)
2539 .collect::<Option<Vec<_>>>()?;
2540 let policy_templates = self
2541 .templates
2542 .values()
2543 .sorted_by_key(|t| AsRef::<str>::as_ref(t.id()))
2544 .map(Template::to_cedar)
2545 .collect_vec();
2546
2547 Some(StringifiedPolicySet {
2548 policies,
2549 policy_templates,
2550 })
2551 }
2552
2553 /// Create a fresh empty `PolicySet`
2554 pub fn new() -> Self {
2555 Self {
2556 ast: ast::PolicySet::new(),
2557 policies: LinkedHashMap::new(),
2558 templates: LinkedHashMap::new(),
2559 }
2560 }
2561
2562 /// Create a `PolicySet` from the given policies
2563 pub fn from_policies(
2564 policies: impl IntoIterator<Item = Policy>,
2565 ) -> Result<Self, PolicySetError> {
2566 let mut set = Self::new();
2567 for policy in policies {
2568 set.add(policy)?;
2569 }
2570 Ok(set)
2571 }
2572
2573 /// Merges this `PolicySet` with another `PolicySet`.
2574 /// This `PolicySet` is modified while the other `PolicySet`
2575 /// remains unchanged.
2576 ///
2577 /// The flag `rename_duplicates` controls the expected behavior
2578 /// when a `PolicyId` in this and the other `PolicySet` conflict.
2579 ///
2580 /// When `rename_duplicates` is false, conflicting `PolicyId`s result
2581 /// in a `PolicySetError::AlreadyDefined` error.
2582 ///
2583 /// Otherwise, when `rename_duplicates` is true, conflicting `PolicyId`s from
2584 /// the other `PolicySet` are automatically renamed to avoid conflict.
2585 /// This renaming is returned as a Hashmap from the old `PolicyId` to the
2586 /// renamed `PolicyId`.
2587 pub fn merge(
2588 &mut self,
2589 other: &Self,
2590 rename_duplicates: bool,
2591 ) -> Result<HashMap<PolicyId, PolicyId>, PolicySetError> {
2592 match self.ast.merge_policyset(&other.ast, rename_duplicates) {
2593 Ok(renaming) => {
2594 let renaming: HashMap<PolicyId, PolicyId> = renaming
2595 .into_iter()
2596 .map(|(old_pid, new_pid)| (PolicyId::new(old_pid), PolicyId::new(new_pid)))
2597 .collect();
2598
2599 for (pid, op) in &other.policies {
2600 let pid = renaming.get(pid).unwrap_or(pid);
2601 if !self.policies.contains_key(pid) {
2602 // PANIC SAFETY: `pid` is the new id of a policy from `other`, so it will be in `self` after merging.
2603 #[allow(clippy::unwrap_used)]
2604 let new_p = Policy {
2605 // Use the representation from `self.ast` so that we get a version with internal references to
2606 // policy ids updated to account for the renaming.
2607 ast: self.ast.get(pid.as_ref()).unwrap().clone(),
2608 lossless: op.lossless.clone(),
2609 };
2610 self.policies.insert(pid.clone(), new_p);
2611 }
2612 }
2613 for (pid, ot) in &other.templates {
2614 let pid = renaming.get(pid).unwrap_or(pid);
2615 if !self.templates.contains_key(pid) {
2616 // PANIC SAFETY: `pid` is the new id of a template from `other`, so it will be in `self` after merging.
2617 #[allow(clippy::unwrap_used)]
2618 let new_t = Template {
2619 ast: self.ast.get_template(pid.as_ref()).unwrap().clone(),
2620 lossless: ot.lossless.clone(),
2621 };
2622 self.templates.insert(pid.clone(), new_t);
2623 }
2624 }
2625
2626 Ok(renaming)
2627 }
2628 Err(ast::PolicySetError::Occupied { id }) => Err(PolicySetError::AlreadyDefined(
2629 policy_set_errors::AlreadyDefined {
2630 id: PolicyId::new(id),
2631 },
2632 )),
2633 }
2634 }
2635
2636 /// Add an static policy to the `PolicySet`. To add a template instance, use
2637 /// `link` instead. This function will return an error (and not modify
2638 /// the `PolicySet`) if a template-linked policy is passed in.
2639 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
2640 if policy.is_static() {
2641 let id = PolicyId::new(policy.ast.id().clone());
2642 self.ast.add(policy.ast.clone())?;
2643 self.policies.insert(id, policy);
2644 Ok(())
2645 } else {
2646 Err(PolicySetError::ExpectedStatic(
2647 policy_set_errors::ExpectedStatic::new(),
2648 ))
2649 }
2650 }
2651
2652 /// Remove a static `Policy` from the `PolicySet`.
2653 ///
2654 /// This will error if the policy is not a static policy.
2655 pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2656 let Some(policy) = self.policies.remove(&policy_id) else {
2657 return Err(PolicySetError::PolicyNonexistent(
2658 policy_set_errors::PolicyNonexistentError { policy_id },
2659 ));
2660 };
2661 if self
2662 .ast
2663 .remove_static(&ast::PolicyID::from_string(&policy_id))
2664 .is_ok()
2665 {
2666 Ok(policy)
2667 } else {
2668 //Restore self.policies
2669 self.policies.insert(policy_id.clone(), policy);
2670 Err(PolicySetError::PolicyNonexistent(
2671 policy_set_errors::PolicyNonexistentError { policy_id },
2672 ))
2673 }
2674 }
2675
2676 /// Add a `Template` to the `PolicySet`
2677 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
2678 let id = PolicyId::new(template.ast.id().clone());
2679 self.ast.add_template(template.ast.clone())?;
2680 self.templates.insert(id, template);
2681 Ok(())
2682 }
2683
2684 /// Remove a `Template` from the `PolicySet`.
2685 ///
2686 /// This will error if any policy is linked to the template.
2687 /// This will error if `policy_id` is not a template.
2688 pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
2689 let Some(template) = self.templates.remove(&template_id) else {
2690 return Err(PolicySetError::TemplateNonexistent(
2691 policy_set_errors::TemplateNonexistentError { template_id },
2692 ));
2693 };
2694 // If self.templates and self.ast disagree, authorization cannot be trusted.
2695 // PANIC SAFETY: We just found the policy in self.templates.
2696 #[allow(clippy::panic)]
2697 match self
2698 .ast
2699 .remove_template(&ast::PolicyID::from_string(&template_id))
2700 {
2701 Ok(_) => Ok(template),
2702 Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
2703 self.templates.insert(template_id.clone(), template);
2704 Err(PolicySetError::RemoveTemplateWithActiveLinks(
2705 policy_set_errors::RemoveTemplateWithActiveLinksError { template_id },
2706 ))
2707 }
2708 Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
2709 self.templates.insert(template_id.clone(), template);
2710 Err(PolicySetError::RemoveTemplateNotTemplate(
2711 policy_set_errors::RemoveTemplateNotTemplateError { template_id },
2712 ))
2713 }
2714 Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
2715 panic!("Found template policy in self.templates but not in self.ast");
2716 }
2717 }
2718 }
2719
2720 /// Get policies linked to a `Template` in the `PolicySet`.
2721 /// If any policy is linked to the template, this will error
2722 pub fn get_linked_policies(
2723 &self,
2724 template_id: PolicyId,
2725 ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
2726 self.ast
2727 .get_linked_policies(&ast::PolicyID::from_string(&template_id))
2728 .map_or_else(
2729 |_| {
2730 Err(PolicySetError::TemplateNonexistent(
2731 policy_set_errors::TemplateNonexistentError { template_id },
2732 ))
2733 },
2734 |v| Ok(v.map(PolicyId::ref_cast)),
2735 )
2736 }
2737
2738 /// Iterate over all the `Policy`s in the `PolicySet`.
2739 ///
2740 /// This will include both static and template-linked policies.
2741 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
2742 self.policies.values()
2743 }
2744
2745 /// Iterate over the `Template`'s in the `PolicySet`.
2746 pub fn templates(&self) -> impl Iterator<Item = &Template> {
2747 self.templates.values()
2748 }
2749
2750 /// Get a `Template` by its `PolicyId`
2751 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
2752 self.templates.get(id)
2753 }
2754
2755 /// Get a `Policy` by its `PolicyId`
2756 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
2757 self.policies.get(id)
2758 }
2759
2760 /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key.
2761 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2762 /// then this function returns `Some("")`. It returns `None` only when the
2763 /// annotation is not present.
2764 pub fn annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2765 self.ast
2766 .get(id.as_ref())?
2767 .annotation(&key.as_ref().parse().ok()?)
2768 .map(AsRef::as_ref)
2769 }
2770
2771 /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
2772 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2773 /// then this function returns `Some("")`. It returns `None` only when the
2774 /// annotation is not present.
2775 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2776 self.ast
2777 .get_template(id.as_ref())?
2778 .annotation(&key.as_ref().parse().ok()?)
2779 .map(AsRef::as_ref)
2780 }
2781
2782 /// Returns true iff the `PolicySet` is empty
2783 pub fn is_empty(&self) -> bool {
2784 debug_assert_eq!(
2785 self.ast.is_empty(),
2786 self.policies.is_empty() && self.templates.is_empty()
2787 );
2788 self.ast.is_empty()
2789 }
2790
2791 /// Returns the number of `Policy`s in the `PolicySet`.
2792 ///
2793 /// This will include both static and template-linked policies.
2794 pub fn num_of_policies(&self) -> usize {
2795 self.policies.len()
2796 }
2797
2798 /// Returns the number of `Template`s in the `PolicySet`.
2799 pub fn num_of_templates(&self) -> usize {
2800 self.templates.len()
2801 }
2802
2803 /// Attempt to link a template and add the new template-linked policy to the policy set.
2804 /// If link fails, the `PolicySet` is not modified.
2805 /// Failure can happen for three reasons
2806 /// 1) The map passed in `vals` may not match the slots in the template
2807 /// 2) The `new_id` may conflict w/ a policy that already exists in the set
2808 /// 3) `template_id` does not correspond to a template. Either the id is
2809 /// not in the policy set, or it is in the policy set but is either a
2810 /// linked or static policy rather than a template
2811 #[allow(clippy::needless_pass_by_value)]
2812 pub fn link(
2813 &mut self,
2814 template_id: PolicyId,
2815 new_id: PolicyId,
2816 vals: HashMap<SlotId, EntityUid>,
2817 ) -> Result<(), PolicySetError> {
2818 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
2819 .into_iter()
2820 .map(|(key, value)| (key.into(), value.into()))
2821 .collect();
2822
2823 // Try to get the template with the id we're linking from. We do this
2824 // _before_ calling `self.ast.link` because `link` mutates the policy
2825 // set by creating a new link entry in a hashmap. This happens even when
2826 // trying to link a static policy, which we want to error on here.
2827 let Some(template) = self.templates.get(&template_id) else {
2828 return Err(if self.policies.contains_key(&template_id) {
2829 policy_set_errors::ExpectedTemplate::new().into()
2830 } else {
2831 policy_set_errors::LinkingError {
2832 inner: ast::LinkingError::NoSuchTemplate {
2833 id: template_id.into(),
2834 },
2835 }
2836 .into()
2837 });
2838 };
2839
2840 let linked_ast = self.ast.link(
2841 template_id.into(),
2842 new_id.clone().into(),
2843 unwrapped_vals.clone(),
2844 )?;
2845
2846 // PANIC SAFETY: `lossless.link()` will not fail after `ast.link()` succeeds
2847 #[allow(clippy::expect_used)]
2848 let linked_lossless = template
2849 .lossless
2850 .clone()
2851 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
2852 // The only error case for `lossless.link()` is a template with
2853 // slots which are not filled by the provided values. `ast.link()`
2854 // will have already errored if there are any unfilled slots in the
2855 // template.
2856 .expect("ast.link() didn't fail above, so this shouldn't fail");
2857 self.policies.insert(
2858 new_id,
2859 Policy {
2860 ast: linked_ast.clone(),
2861 lossless: linked_lossless,
2862 },
2863 );
2864 Ok(())
2865 }
2866
2867 /// Get all the unknown entities from the policy set
2868 #[doc = include_str!("../experimental_warning.md")]
2869 #[cfg(feature = "partial-eval")]
2870 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
2871 let mut entity_uids = HashSet::new();
2872 for policy in self.policies.values() {
2873 entity_uids.extend(policy.unknown_entities());
2874 }
2875 entity_uids
2876 }
2877
2878 /// Unlink a template-linked policy from the policy set.
2879 /// Returns the policy that was unlinked.
2880 pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2881 let Some(policy) = self.policies.remove(&policy_id) else {
2882 return Err(PolicySetError::LinkNonexistent(
2883 policy_set_errors::LinkNonexistentError { policy_id },
2884 ));
2885 };
2886 // If self.policies and self.ast disagree, authorization cannot be trusted.
2887 // PANIC SAFETY: We just found the policy in self.policies.
2888 #[allow(clippy::panic)]
2889 match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
2890 Ok(_) => Ok(policy),
2891 Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
2892 //Restore self.policies
2893 self.policies.insert(policy_id.clone(), policy);
2894 Err(PolicySetError::UnlinkLinkNotLink(
2895 policy_set_errors::UnlinkLinkNotLinkError { policy_id },
2896 ))
2897 }
2898 Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
2899 panic!("Found linked policy in self.policies but not in self.ast")
2900 }
2901 }
2902 }
2903}
2904
2905impl std::fmt::Display for PolicySet {
2906 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2907 // prefer to display the lossless format
2908 let mut policies = self.policies().peekable();
2909 while let Some(policy) = policies.next() {
2910 policy.lossless.fmt(|| policy.ast.clone().into(), f)?;
2911 if policies.peek().is_some() {
2912 writeln!(f)?;
2913 }
2914 }
2915 Ok(())
2916 }
2917}
2918
2919/// Given a [`PolicyId`] and a [`Policy`], determine if the policy represents a static policy or a
2920/// link
2921fn is_static_or_link(
2922 (id, policy): (PolicyId, Policy),
2923) -> Result<Either<(ast::PolicyID, est::Policy), TemplateLink>, PolicyToJsonError> {
2924 match policy.template_id() {
2925 Some(template_id) => {
2926 let values = policy
2927 .ast
2928 .env()
2929 .iter()
2930 .map(|(id, euid)| (*id, euid.clone()))
2931 .collect();
2932 Ok(Either::Right(TemplateLink {
2933 new_id: id.into(),
2934 template_id: template_id.clone().into(),
2935 values,
2936 }))
2937 }
2938 None => policy
2939 .lossless
2940 .est(|| policy.ast.clone().into())
2941 .map(|est| Either::Left((id.into(), est))),
2942 }
2943}
2944
2945/// Like [`itertools::Itertools::partition_map`], but accepts a function that can fail.
2946/// The first invocation of `f` that fails causes the whole computation to fail
2947#[allow(clippy::redundant_pub_crate)] // can't be private because it's used in tests
2948pub(crate) fn fold_partition<T, A, B, E>(
2949 i: impl IntoIterator<Item = T>,
2950 f: impl Fn(T) -> Result<Either<A, B>, E>,
2951) -> Result<(Vec<A>, Vec<B>), E> {
2952 let mut lefts = vec![];
2953 let mut rights = vec![];
2954
2955 for item in i {
2956 match f(item)? {
2957 Either::Left(left) => lefts.push(left),
2958 Either::Right(right) => rights.push(right),
2959 }
2960 }
2961
2962 Ok((lefts, rights))
2963}
2964
2965/// The "type" of a [`Request`], i.e., the [`EntityTypeName`]s of principal
2966/// and resource, the [`EntityUid`] of action, and [`Option<EntityTypeName>`]s
2967/// of principal slot and resource slot
2968#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2969pub struct RequestEnv {
2970 pub(crate) principal: EntityTypeName,
2971 pub(crate) action: EntityUid,
2972 pub(crate) resource: EntityTypeName,
2973 pub(crate) principal_slot: Option<EntityTypeName>,
2974 pub(crate) resource_slot: Option<EntityTypeName>,
2975}
2976
2977impl RequestEnv {
2978 /// Construct a [`RequestEnv`]
2979 pub fn new(principal: EntityTypeName, action: EntityUid, resource: EntityTypeName) -> Self {
2980 Self {
2981 principal,
2982 action,
2983 resource,
2984 principal_slot: None,
2985 resource_slot: None,
2986 }
2987 }
2988
2989 /// Construct a [`RequestEnv`] that contains slots in the scope
2990 pub fn new_request_env_with_slots(
2991 principal: EntityTypeName,
2992 action: EntityUid,
2993 resource: EntityTypeName,
2994 principal_slot: Option<EntityTypeName>,
2995 resource_slot: Option<EntityTypeName>,
2996 ) -> Self {
2997 Self {
2998 principal,
2999 action,
3000 resource,
3001 principal_slot,
3002 resource_slot,
3003 }
3004 }
3005
3006 /// Get the principal type name
3007 pub fn principal(&self) -> &EntityTypeName {
3008 &self.principal
3009 }
3010
3011 /// Get the action [`EntityUid`]
3012 pub fn action(&self) -> &EntityUid {
3013 &self.action
3014 }
3015
3016 /// Get the resource type name
3017 pub fn resource(&self) -> &EntityTypeName {
3018 &self.resource
3019 }
3020
3021 /// Get the principal slot type name
3022 pub fn principal_slot(&self) -> Option<&EntityTypeName> {
3023 self.principal_slot.as_ref()
3024 }
3025
3026 /// Get the resource slot type name
3027 pub fn resource_slot(&self) -> Option<&EntityTypeName> {
3028 self.resource_slot.as_ref()
3029 }
3030}
3031
3032#[doc(hidden)]
3033impl From<cedar_policy_core::validator::types::RequestEnv<'_>> for RequestEnv {
3034 fn from(renv: cedar_policy_core::validator::types::RequestEnv<'_>) -> Self {
3035 match renv {
3036 cedar_policy_core::validator::types::RequestEnv::DeclaredAction {
3037 principal,
3038 action,
3039 resource,
3040 principal_slot,
3041 resource_slot,
3042 ..
3043 } => Self {
3044 principal: principal.clone().into(),
3045 action: action.clone().into(),
3046 resource: resource.clone().into(),
3047 principal_slot: principal_slot.map(EntityTypeName::from),
3048 resource_slot: resource_slot.map(EntityTypeName::from),
3049 },
3050 // PANIC SAFETY: partial validation is not enabled and hence `RequestEnv::UndeclaredAction` should not show up
3051 #[allow(clippy::unreachable)]
3052 cedar_policy_core::validator::types::RequestEnv::UndeclaredAction => {
3053 unreachable!("used unsupported feature")
3054 }
3055 }
3056 }
3057}
3058
3059/// Get valid request envs for an `ast::Template`
3060///
3061/// This function is called by [`Template::get_valid_request_envs`] and
3062/// [`Policy::get_valid_request_envs`]
3063fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3064 let tc = Typechecker::new(
3065 &s.0,
3066 cedar_policy_core::validator::ValidationMode::default(),
3067 );
3068 tc.typecheck_by_request_env(ast)
3069 .into_iter()
3070 .filter_map(|(env, pc)| {
3071 if matches!(pc, PolicyCheck::Success(_)) {
3072 Some(env.into())
3073 } else {
3074 None
3075 }
3076 })
3077 .collect::<BTreeSet<_>>()
3078 .into_iter()
3079}
3080
3081/// Policy template datatype
3082//
3083// NOTE: Unlike the internal type [`ast::Template`], this type only supports
3084// templates. The `Template` constructors will return an error if provided with
3085// a static policy.
3086#[derive(Debug, Clone)]
3087pub struct Template {
3088 /// AST representation of the template, used for most operations.
3089 /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
3090 pub(crate) ast: ast::Template,
3091
3092 /// Some "lossless" representation of the template, whichever is most
3093 /// convenient to provide (and can be provided with the least overhead).
3094 /// This is used just for `to_json()`.
3095 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3096 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3097 /// we can from the EST (modulo whitespace and a few other things like the
3098 /// order of annotations).
3099 ///
3100 /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
3101 /// because the EST doesn't distinguish between static policies and templates.
3102 pub(crate) lossless: LosslessPolicy,
3103}
3104
3105impl PartialEq for Template {
3106 fn eq(&self, other: &Self) -> bool {
3107 // eq is based on just the `ast`
3108 self.ast.eq(&other.ast)
3109 }
3110}
3111impl Eq for Template {}
3112
3113#[doc(hidden)] // because this converts to a private/internal type
3114impl AsRef<ast::Template> for Template {
3115 fn as_ref(&self) -> &ast::Template {
3116 &self.ast
3117 }
3118}
3119
3120#[doc(hidden)]
3121impl From<ast::Template> for Template {
3122 fn from(template: ast::Template) -> Self {
3123 Self::from_ast(template)
3124 }
3125}
3126
3127impl Template {
3128 /// Attempt to parse a [`Template`] from source.
3129 /// Returns an error if the input is a static policy (i.e., has no slots).
3130 /// If `id` is Some, then the resulting template will have that `id`.
3131 /// If the `id` is None, the parser will use the default "policy0".
3132 /// The behavior around None may change in the future.
3133 pub fn parse(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3134 let ast = parser::parse_template(id.map(Into::into), src.as_ref())?;
3135 Ok(Self {
3136 ast,
3137 lossless: LosslessPolicy::policy_or_template_text(Some(src.as_ref())),
3138 })
3139 }
3140
3141 /// Get the `PolicyId` of this `Template`
3142 pub fn id(&self) -> &PolicyId {
3143 PolicyId::ref_cast(self.ast.id())
3144 }
3145
3146 /// Clone this `Template` with a new `PolicyId`
3147 #[must_use]
3148 pub fn new_id(&self, id: PolicyId) -> Self {
3149 Self {
3150 ast: self.ast.new_id(id.into()),
3151 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3152 }
3153 }
3154
3155 /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
3156 pub fn effect(&self) -> Effect {
3157 self.ast.effect()
3158 }
3159
3160 /// Returns `true` if this template has a `when` or `unless` clause.
3161 pub fn has_non_scope_constraint(&self) -> bool {
3162 self.ast.non_scope_constraints().is_some()
3163 }
3164
3165 /// Get an annotation value of this `Template`.
3166 /// If the annotation is present without an explicit value (e.g., `@annotation`),
3167 /// then this function returns `Some("")`. Returns `None` when the
3168 /// annotation is not present or when `key` is not a valid annotation identifier.
3169 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3170 self.ast
3171 .annotation(&key.as_ref().parse().ok()?)
3172 .map(AsRef::as_ref)
3173 }
3174
3175 /// Iterate through annotation data of this `Template` as key-value pairs.
3176 /// Annotations which do not have an explicit value (e.g., `@annotation`),
3177 /// are included in the iterator with the value `""`.
3178 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3179 self.ast
3180 .annotations()
3181 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3182 }
3183
3184 /// Iterate over the open slots in this `Template`
3185 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
3186 self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
3187 }
3188
3189 /// Get the scope constraint on this policy's principal
3190 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
3191 match self.ast.principal_constraint().as_inner() {
3192 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
3193 ast::PrincipalOrResourceConstraint::In(eref) => {
3194 TemplatePrincipalConstraint::In(match eref {
3195 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3196 ast::EntityReference::Slot(_) => None,
3197 })
3198 }
3199 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3200 TemplatePrincipalConstraint::Eq(match eref {
3201 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3202 ast::EntityReference::Slot(_) => None,
3203 })
3204 }
3205 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3206 TemplatePrincipalConstraint::Is(entity_type.as_ref().clone().into())
3207 }
3208 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3209 TemplatePrincipalConstraint::IsIn(
3210 entity_type.as_ref().clone().into(),
3211 match eref {
3212 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3213 ast::EntityReference::Slot(_) => None,
3214 },
3215 )
3216 }
3217 }
3218 }
3219
3220 /// Get the scope constraint on this policy's action
3221 pub fn action_constraint(&self) -> ActionConstraint {
3222 // Clone the data from Core to be consistent with the other constraints
3223 match self.ast.action_constraint() {
3224 ast::ActionConstraint::Any => ActionConstraint::Any,
3225 ast::ActionConstraint::In(ids) => {
3226 ActionConstraint::In(ids.iter().map(|id| id.as_ref().clone().into()).collect())
3227 }
3228 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(id.as_ref().clone().into()),
3229 #[cfg(feature = "tolerant-ast")]
3230 ast::ActionConstraint::ErrorConstraint => {
3231 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3232 // It is not recommended to evaluate an AST that allows error nodes
3233 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3234 // treat it as `Any`
3235 ActionConstraint::Any
3236 }
3237 }
3238 }
3239
3240 /// Get the scope constraint on this policy's resource
3241 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
3242 match self.ast.resource_constraint().as_inner() {
3243 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
3244 ast::PrincipalOrResourceConstraint::In(eref) => {
3245 TemplateResourceConstraint::In(match eref {
3246 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3247 ast::EntityReference::Slot(_) => None,
3248 })
3249 }
3250 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3251 TemplateResourceConstraint::Eq(match eref {
3252 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3253 ast::EntityReference::Slot(_) => None,
3254 })
3255 }
3256 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3257 TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3258 }
3259 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3260 TemplateResourceConstraint::IsIn(
3261 entity_type.as_ref().clone().into(),
3262 match eref {
3263 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3264 ast::EntityReference::Slot(_) => None,
3265 },
3266 )
3267 }
3268 }
3269 }
3270
3271 /// Create a [`Template`] from its JSON representation.
3272 /// Returns an error if the input is a static policy (i.e., has no slots).
3273 /// If `id` is Some, the policy will be given that Policy Id.
3274 /// If `id` is None, then "JSON policy" will be used.
3275 /// The behavior around None may change in the future.
3276 pub fn from_json(
3277 id: Option<PolicyId>,
3278 json: serde_json::Value,
3279 ) -> Result<Self, PolicyFromJsonError> {
3280 let est: est::Policy = serde_json::from_value(json)
3281 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3282 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3283 Self::from_est(id, est)
3284 }
3285
3286 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3287 Ok(Self {
3288 ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3289 lossless: LosslessPolicy::Est(est),
3290 })
3291 }
3292
3293 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
3294 pub(crate) fn from_ast(ast: ast::Template) -> Self {
3295 Self {
3296 lossless: LosslessPolicy::Est(ast.clone().into()),
3297 ast,
3298 }
3299 }
3300
3301 /// Get the JSON representation of this `Template`.
3302 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3303 let est = self.lossless.est(|| self.ast.clone().into())?;
3304 serde_json::to_value(est).map_err(Into::into)
3305 }
3306
3307 /// Get the human-readable Cedar syntax representation of this template.
3308 /// This function is primarily intended for rendering JSON policies in the
3309 /// human-readable syntax, but it will also return the original policy text
3310 /// when given a policy parsed from the human-readable syntax.
3311 ///
3312 /// It also does not format the policy according to any particular rules.
3313 /// Policy formatting can be done through the Cedar policy CLI or
3314 /// the `cedar-policy-formatter` crate.
3315 pub fn to_cedar(&self) -> String {
3316 match &self.lossless {
3317 LosslessPolicy::Empty | LosslessPolicy::Est(_) => self.ast.to_string(),
3318 LosslessPolicy::Text { text, .. } => text.clone(),
3319 }
3320 }
3321
3322 /// Get the valid [`RequestEnv`]s for this template, according to the schema.
3323 ///
3324 /// That is, all the [`RequestEnv`]s in the schema for which this template is
3325 /// not trivially false.
3326 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3327 get_valid_request_envs(&self.ast, s)
3328 }
3329}
3330
3331impl std::fmt::Display for Template {
3332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3333 // prefer to display the lossless format
3334 self.lossless.fmt(|| self.ast.clone().into(), f)
3335 }
3336}
3337
3338impl FromStr for Template {
3339 type Err = ParseErrors;
3340
3341 fn from_str(src: &str) -> Result<Self, Self::Err> {
3342 Self::parse(None, src)
3343 }
3344}
3345
3346/// Scope constraint on policy principals.
3347#[derive(Debug, Clone, PartialEq, Eq)]
3348pub enum PrincipalConstraint {
3349 /// Un-constrained
3350 Any,
3351 /// Must be In the given [`EntityUid`]
3352 In(EntityUid),
3353 /// Must be equal to the given [`EntityUid`]
3354 Eq(EntityUid),
3355 /// Must be the given [`EntityTypeName`]
3356 Is(EntityTypeName),
3357 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3358 IsIn(EntityTypeName, EntityUid),
3359}
3360
3361/// Scope constraint on policy principals for templates.
3362#[derive(Debug, Clone, PartialEq, Eq)]
3363pub enum TemplatePrincipalConstraint {
3364 /// Un-constrained
3365 Any,
3366 /// Must be In the given [`EntityUid`].
3367 /// If [`None`], then it is a template slot.
3368 In(Option<EntityUid>),
3369 /// Must be equal to the given [`EntityUid`].
3370 /// If [`None`], then it is a template slot.
3371 Eq(Option<EntityUid>),
3372 /// Must be the given [`EntityTypeName`].
3373 Is(EntityTypeName),
3374 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3375 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3376 IsIn(EntityTypeName, Option<EntityUid>),
3377}
3378
3379impl TemplatePrincipalConstraint {
3380 /// Does this constraint contain a slot?
3381 pub fn has_slot(&self) -> bool {
3382 match self {
3383 Self::Any | Self::Is(_) => false,
3384 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3385 }
3386 }
3387}
3388
3389/// Scope constraint on policy actions.
3390#[derive(Debug, Clone, PartialEq, Eq)]
3391pub enum ActionConstraint {
3392 /// Un-constrained
3393 Any,
3394 /// Must be In the given [`EntityUid`]
3395 In(Vec<EntityUid>),
3396 /// Must be equal to the given [`EntityUid]`
3397 Eq(EntityUid),
3398}
3399
3400/// Scope constraint on policy resources.
3401#[derive(Debug, Clone, PartialEq, Eq)]
3402pub enum ResourceConstraint {
3403 /// Un-constrained
3404 Any,
3405 /// Must be In the given [`EntityUid`]
3406 In(EntityUid),
3407 /// Must be equal to the given [`EntityUid`]
3408 Eq(EntityUid),
3409 /// Must be the given [`EntityTypeName`]
3410 Is(EntityTypeName),
3411 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3412 IsIn(EntityTypeName, EntityUid),
3413}
3414
3415/// Scope constraint on policy resources for templates.
3416#[derive(Debug, Clone, PartialEq, Eq)]
3417pub enum TemplateResourceConstraint {
3418 /// Un-constrained
3419 Any,
3420 /// Must be In the given [`EntityUid`].
3421 /// If [`None`], then it is a template slot.
3422 In(Option<EntityUid>),
3423 /// Must be equal to the given [`EntityUid`].
3424 /// If [`None`], then it is a template slot.
3425 Eq(Option<EntityUid>),
3426 /// Must be the given [`EntityTypeName`].
3427 Is(EntityTypeName),
3428 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3429 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3430 IsIn(EntityTypeName, Option<EntityUid>),
3431}
3432
3433impl TemplateResourceConstraint {
3434 /// Does this constraint contain a slot?
3435 pub fn has_slot(&self) -> bool {
3436 match self {
3437 Self::Any | Self::Is(_) => false,
3438 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3439 }
3440 }
3441}
3442
3443/// Structure for a `Policy`. Includes both static policies and template-linked policies.
3444#[derive(Debug, Clone)]
3445pub struct Policy {
3446 /// AST representation of the policy, used for most operations.
3447 /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
3448 pub(crate) ast: ast::Policy,
3449 /// Some "lossless" representation of the policy, whichever is most
3450 /// convenient to provide (and can be provided with the least overhead).
3451 /// This is used just for `to_json()`.
3452 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3453 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3454 /// we can from the EST (modulo whitespace and a few other things like the
3455 /// order of annotations).
3456 pub(crate) lossless: LosslessPolicy,
3457}
3458
3459impl PartialEq for Policy {
3460 fn eq(&self, other: &Self) -> bool {
3461 // eq is based on just the `ast`
3462 self.ast.eq(&other.ast)
3463 }
3464}
3465impl Eq for Policy {}
3466
3467#[doc(hidden)] // because this converts to a private/internal type
3468impl AsRef<ast::Policy> for Policy {
3469 fn as_ref(&self) -> &ast::Policy {
3470 &self.ast
3471 }
3472}
3473
3474#[doc(hidden)]
3475impl From<ast::Policy> for Policy {
3476 fn from(policy: ast::Policy) -> Self {
3477 Self::from_ast(policy)
3478 }
3479}
3480
3481#[doc(hidden)]
3482impl From<ast::StaticPolicy> for Policy {
3483 fn from(policy: ast::StaticPolicy) -> Self {
3484 ast::Policy::from(policy).into()
3485 }
3486}
3487
3488impl Policy {
3489 /// Get the `PolicyId` of the `Template` this is linked to.
3490 /// If this is a static policy, this will return `None`.
3491 pub fn template_id(&self) -> Option<&PolicyId> {
3492 if self.is_static() {
3493 None
3494 } else {
3495 Some(PolicyId::ref_cast(self.ast.template().id()))
3496 }
3497 }
3498
3499 /// Get the values this `Template` is linked to, expressed as a map from `SlotId` to `EntityUid`.
3500 /// If this is a static policy, this will return `None`.
3501 pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3502 if self.is_static() {
3503 None
3504 } else {
3505 let wrapped_vals: HashMap<SlotId, EntityUid> = self
3506 .ast
3507 .env()
3508 .iter()
3509 .map(|(key, value)| ((*key).into(), value.clone().into()))
3510 .collect();
3511 Some(wrapped_vals)
3512 }
3513 }
3514
3515 /// Get the `Effect` (`Permit` or `Forbid`) for this instance
3516 pub fn effect(&self) -> Effect {
3517 self.ast.effect()
3518 }
3519
3520 /// Returns `true` if this policy has a `when` or `unless` clause.
3521 pub fn has_non_scope_constraint(&self) -> bool {
3522 self.ast.non_scope_constraints().is_some()
3523 }
3524
3525 /// Get an annotation value of this template-linked or static policy.
3526 /// If the annotation is present without an explicit value (e.g., `@annotation`),
3527 /// then this function returns `Some("")`. Returns `None` when the
3528 /// annotation is not present or when `key` is not a valid annotations identifier.
3529 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3530 self.ast
3531 .annotation(&key.as_ref().parse().ok()?)
3532 .map(AsRef::as_ref)
3533 }
3534
3535 /// Iterate through annotation data of this template-linked or static policy.
3536 /// Annotations which do not have an explicit value (e.g., `@annotation`),
3537 /// are included in the iterator with the value `""`.
3538 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3539 self.ast
3540 .annotations()
3541 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3542 }
3543
3544 /// Get the `PolicyId` for this template-linked or static policy
3545 pub fn id(&self) -> &PolicyId {
3546 PolicyId::ref_cast(self.ast.id())
3547 }
3548
3549 /// Clone this `Policy` with a new `PolicyId`
3550 #[must_use]
3551 pub fn new_id(&self, id: PolicyId) -> Self {
3552 Self {
3553 ast: self.ast.new_id(id.into()),
3554 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3555 }
3556 }
3557
3558 /// Returns `true` if this is a static policy, `false` otherwise.
3559 pub fn is_static(&self) -> bool {
3560 self.ast.is_static()
3561 }
3562
3563 /// Get the scope constraint on this policy's principal
3564 pub fn principal_constraint(&self) -> PrincipalConstraint {
3565 let slot_id = ast::SlotId::principal();
3566 match self.ast.template().principal_constraint().as_inner() {
3567 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3568 ast::PrincipalOrResourceConstraint::In(eref) => {
3569 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3570 }
3571 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3572 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3573 }
3574 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3575 PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3576 }
3577 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3578 PrincipalConstraint::IsIn(
3579 entity_type.as_ref().clone().into(),
3580 self.convert_entity_reference(eref, slot_id).clone(),
3581 )
3582 }
3583 }
3584 }
3585
3586 /// Get the scope constraint on this policy's action
3587 pub fn action_constraint(&self) -> ActionConstraint {
3588 // Clone the data from Core to be consistant with the other constraints
3589 match self.ast.template().action_constraint() {
3590 ast::ActionConstraint::Any => ActionConstraint::Any,
3591 ast::ActionConstraint::In(ids) => ActionConstraint::In(
3592 ids.iter()
3593 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3594 .cloned()
3595 .collect(),
3596 ),
3597 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3598 #[cfg(feature = "tolerant-ast")]
3599 ast::ActionConstraint::ErrorConstraint => {
3600 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3601 // It is not recommended to evaluate an AST that allows error nodes
3602 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3603 // treat it as `Any`
3604 ActionConstraint::Any
3605 }
3606 }
3607 }
3608
3609 /// Get the scope constraint on this policy's resource
3610 pub fn resource_constraint(&self) -> ResourceConstraint {
3611 let slot_id = ast::SlotId::resource();
3612 match self.ast.template().resource_constraint().as_inner() {
3613 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3614 ast::PrincipalOrResourceConstraint::In(eref) => {
3615 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3616 }
3617 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3618 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3619 }
3620 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3621 ResourceConstraint::Is(entity_type.as_ref().clone().into())
3622 }
3623 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3624 ResourceConstraint::IsIn(
3625 entity_type.as_ref().clone().into(),
3626 self.convert_entity_reference(eref, slot_id).clone(),
3627 )
3628 }
3629 }
3630 }
3631
3632 /// To avoid panicking, this function may only be called when `slot` is the
3633 /// `SlotId` corresponding to the scope constraint from which the entity
3634 /// reference `r` was extracted. I.e., If `r` is taken from the principal
3635 /// scope constraint, `slot` must be `?principal`. This ensures that the
3636 /// `SlotId` exists in the policy (and therefore the slot environment map)
3637 /// whenever the `EntityReference` `r` is the Slot variant.
3638 fn convert_entity_reference<'a>(
3639 &'a self,
3640 r: &'a ast::EntityReference,
3641 slot: ast::SlotId,
3642 ) -> &'a EntityUid {
3643 match r {
3644 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3645 // PANIC SAFETY: This `unwrap` here is safe due the invariant (values total map) on policies.
3646 #[allow(clippy::unwrap_used)]
3647 ast::EntityReference::Slot(_) => {
3648 EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3649 }
3650 }
3651 }
3652
3653 /// Parse a single policy.
3654 /// If `id` is Some, the policy will be given that Policy Id.
3655 /// If `id` is None, then "policy0" will be used.
3656 /// The behavior around None may change in the future.
3657 ///
3658 /// This can fail if the policy fails to parse.
3659 /// It can also fail if a template was passed in, as this function only accepts static
3660 /// policies
3661 pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3662 let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3663 let (_, ast) = ast::Template::link_static_policy(inline_ast);
3664 Ok(Self {
3665 ast,
3666 lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
3667 })
3668 }
3669
3670 /// Create a `Policy` from its JSON representation.
3671 /// If `id` is Some, the policy will be given that Policy Id.
3672 /// If `id` is None, then "JSON policy" will be used.
3673 /// The behavior around None may change in the future.
3674 ///
3675 /// ```
3676 /// # use cedar_policy::{Policy, PolicyId};
3677 ///
3678 /// let json: serde_json::Value = serde_json::json!(
3679 /// {
3680 /// "effect":"permit",
3681 /// "principal":{
3682 /// "op":"==",
3683 /// "entity":{
3684 /// "type":"User",
3685 /// "id":"bob"
3686 /// }
3687 /// },
3688 /// "action":{
3689 /// "op":"==",
3690 /// "entity":{
3691 /// "type":"Action",
3692 /// "id":"view"
3693 /// }
3694 /// },
3695 /// "resource":{
3696 /// "op":"==",
3697 /// "entity":{
3698 /// "type":"Album",
3699 /// "id":"trip"
3700 /// }
3701 /// },
3702 /// "conditions":[
3703 /// {
3704 /// "kind":"when",
3705 /// "body":{
3706 /// ">":{
3707 /// "left":{
3708 /// ".":{
3709 /// "left":{
3710 /// "Var":"principal"
3711 /// },
3712 /// "attr":"age"
3713 /// }
3714 /// },
3715 /// "right":{
3716 /// "Value":18
3717 /// }
3718 /// }
3719 /// }
3720 /// }
3721 /// ]
3722 /// }
3723 /// );
3724 /// let json_policy = Policy::from_json(None, json).unwrap();
3725 /// let src = r#"
3726 /// permit(
3727 /// principal == User::"bob",
3728 /// action == Action::"view",
3729 /// resource == Album::"trip"
3730 /// )
3731 /// when { principal.age > 18 };"#;
3732 /// let text_policy = Policy::parse(None, src).unwrap();
3733 /// assert_eq!(json_policy.to_json().unwrap(), text_policy.to_json().unwrap());
3734 /// ```
3735 pub fn from_json(
3736 id: Option<PolicyId>,
3737 json: serde_json::Value,
3738 ) -> Result<Self, PolicyFromJsonError> {
3739 let est: est::Policy = serde_json::from_value(json)
3740 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3741 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3742 Self::from_est(id, est)
3743 }
3744
3745 /// Get the valid [`RequestEnv`]s for this policy, according to the schema.
3746 ///
3747 /// That is, all the [`RequestEnv`]s in the schema for which this policy is
3748 /// not trivially false.
3749 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3750 get_valid_request_envs(self.ast.template(), s)
3751 }
3752
3753 /// Get all entity literals occuring in a `Policy`
3754 pub fn entity_literals(&self) -> Vec<EntityUid> {
3755 self.ast
3756 .condition()
3757 .subexpressions()
3758 .filter_map(|e| match e.expr_kind() {
3759 cedar_policy_core::ast::ExprKind::Lit(
3760 cedar_policy_core::ast::Literal::EntityUID(euid),
3761 ) => Some(EntityUid((*euid).as_ref().clone())),
3762 _ => None,
3763 })
3764 .collect()
3765 }
3766
3767 /// Return a new policy where all occurrences of key `EntityUid`s are replaced by value `EntityUid`
3768 /// (as a single, non-sequential substitution).
3769 pub fn sub_entity_literals(
3770 &self,
3771 mapping: BTreeMap<EntityUid, EntityUid>,
3772 ) -> Result<Self, PolicyFromJsonError> {
3773 // PANIC SAFETY: This can't fail for a policy that was already constructed
3774 #[allow(clippy::expect_used)]
3775 let cloned_est = self
3776 .lossless
3777 .est(|| self.ast.clone().into())
3778 .expect("Internal error, failed to construct est.");
3779
3780 let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3781
3782 // PANIC SAFETY: This can't fail for a policy that was already constructed
3783 #[allow(clippy::expect_used)]
3784 let est = cloned_est
3785 .sub_entity_literals(&mapping)
3786 .expect("Internal error, failed to sub entity literals.");
3787
3788 let ast = match est.clone().try_into_ast_policy(Some(self.ast.id().clone())) {
3789 Ok(ast) => ast,
3790 Err(e) => return Err(e.into()),
3791 };
3792
3793 Ok(Self {
3794 ast,
3795 lossless: LosslessPolicy::Est(est),
3796 })
3797 }
3798
3799 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3800 Ok(Self {
3801 ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3802 lossless: LosslessPolicy::Est(est),
3803 })
3804 }
3805
3806 /// Get the JSON representation of this `Policy`.
3807 /// ```
3808 /// # use cedar_policy::Policy;
3809 /// let src = r#"
3810 /// permit(
3811 /// principal == User::"bob",
3812 /// action == Action::"view",
3813 /// resource == Album::"trip"
3814 /// )
3815 /// when { principal.age > 18 };"#;
3816 ///
3817 /// let policy = Policy::parse(None, src).unwrap();
3818 /// println!("{}", policy);
3819 /// // convert the policy to JSON
3820 /// let json = policy.to_json().unwrap();
3821 /// println!("{}", json);
3822 /// assert_eq!(json, Policy::from_json(None, json.clone()).unwrap().to_json().unwrap());
3823 /// ```
3824 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3825 let est = self.lossless.est(|| self.ast.clone().into())?;
3826 serde_json::to_value(est).map_err(Into::into)
3827 }
3828
3829 /// Get the human-readable Cedar syntax representation of this policy. This
3830 /// function is primarily intended for rendering JSON policies in the
3831 /// human-readable syntax, but it will also return the original policy text
3832 /// when given a policy parsed from the human-readable syntax.
3833 ///
3834 /// It will return `None` for linked policies because they cannot be
3835 /// directly rendered in Cedar syntax. You can instead render the unlinked
3836 /// template if you do not need to preserve links. If serializing links is
3837 /// important, then you will need to serialize the whole policy set
3838 /// containing the template and link to JSON (or protobuf).
3839 ///
3840 /// It also does not format the policy according to any particular rules.
3841 /// Policy formatting can be done through the Cedar policy CLI or
3842 /// the `cedar-policy-formatter` crate.
3843 pub fn to_cedar(&self) -> Option<String> {
3844 match &self.lossless {
3845 LosslessPolicy::Empty | LosslessPolicy::Est(_) => Some(self.ast.to_string()),
3846 LosslessPolicy::Text { text, slots } => {
3847 if slots.is_empty() {
3848 Some(text.clone())
3849 } else {
3850 None
3851 }
3852 }
3853 }
3854 }
3855
3856 /// Get all the unknown entities from the policy
3857 #[doc = include_str!("../experimental_warning.md")]
3858 #[cfg(feature = "partial-eval")]
3859 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
3860 self.ast
3861 .unknown_entities()
3862 .into_iter()
3863 .map(Into::into)
3864 .collect()
3865 }
3866
3867 /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
3868 /// will reflect the AST structure. When possible, don't use this method and
3869 /// create the `Policy` from the policy text, CST, or EST instead, as the
3870 /// conversion to AST is lossy. ESTs for policies generated by this method
3871 /// will reflect the AST and not the original policy syntax.
3872 #[cfg_attr(
3873 not(any(feature = "partial-eval", feature = "protobufs")),
3874 allow(unused)
3875 )]
3876 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
3877 let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
3878 Self {
3879 ast,
3880 lossless: LosslessPolicy::policy_or_template_text(Some(text)),
3881 }
3882 }
3883}
3884
3885impl std::fmt::Display for Policy {
3886 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3887 // prefer to display the lossless format
3888 self.lossless.fmt(|| self.ast.clone().into(), f)
3889 }
3890}
3891
3892impl FromStr for Policy {
3893 type Err = ParseErrors;
3894 /// Create a policy
3895 ///
3896 /// Important note: Policies have ids, but this interface does not
3897 /// allow them to be set. It will use the default "policy0", which
3898 /// may cause id conflicts if not handled. Use `Policy::parse` to set
3899 /// the id when parsing, or `Policy::new_id` to clone a policy with
3900 /// a new id.
3901 fn from_str(policy: &str) -> Result<Self, Self::Err> {
3902 Self::parse(None, policy)
3903 }
3904}
3905
3906/// See comments on `Policy` and `Template`.
3907///
3908/// This structure can be used for static policies, linked policies, and templates.
3909#[derive(Debug, Clone)]
3910pub(crate) enum LosslessPolicy {
3911 /// An empty representation
3912 Empty,
3913 /// EST representation
3914 Est(est::Policy),
3915 /// Text representation
3916 Text {
3917 /// actual policy text, of the policy or template
3918 text: String,
3919 /// For linked policies, map of slot to UID. Only linked policies have
3920 /// this; static policies and (unlinked) templates have an empty map
3921 /// here
3922 slots: HashMap<ast::SlotId, ast::EntityUID>,
3923 },
3924}
3925
3926impl LosslessPolicy {
3927 /// Create a new `LosslessPolicy` from the text of a policy or template.
3928 fn policy_or_template_text(text: Option<impl Into<String>>) -> Self {
3929 text.map_or(Self::Empty, |text| Self::Text {
3930 text: text.into(),
3931 slots: HashMap::new(),
3932 })
3933 }
3934
3935 /// Get the EST representation of this static policy, linked policy, or template.
3936 fn est(
3937 &self,
3938 fallback_est: impl FnOnce() -> est::Policy,
3939 ) -> Result<est::Policy, PolicyToJsonError> {
3940 match self {
3941 // Fall back to the `policy` AST if the lossless representation is empty
3942 Self::Empty => Ok(fallback_est()),
3943 Self::Est(est) => Ok(est.clone()),
3944 Self::Text { text, slots } => {
3945 let est =
3946 parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
3947 if slots.is_empty() {
3948 Ok(est)
3949 } else {
3950 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
3951 Ok(est.link(&unwrapped_vals)?)
3952 }
3953 }
3954 }
3955 }
3956
3957 fn link<'a>(
3958 self,
3959 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
3960 ) -> Result<Self, est::LinkingError> {
3961 match self {
3962 Self::Empty => Ok(Self::Empty),
3963 Self::Est(est) => {
3964 let unwrapped_est_vals: HashMap<
3965 ast::SlotId,
3966 cedar_policy_core::entities::EntityUidJson,
3967 > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
3968 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
3969 }
3970 Self::Text { text, slots } => {
3971 debug_assert!(
3972 slots.is_empty(),
3973 "shouldn't call link() on an already-linked policy"
3974 );
3975 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
3976 Ok(Self::Text { text, slots })
3977 }
3978 }
3979 }
3980
3981 fn fmt(
3982 &self,
3983 fallback_est: impl FnOnce() -> est::Policy,
3984 f: &mut std::fmt::Formatter<'_>,
3985 ) -> std::fmt::Result {
3986 match self {
3987 Self::Empty => match self.est(fallback_est) {
3988 Ok(est) => write!(f, "{est}"),
3989 Err(e) => write!(f, "<invalid policy: {e}>"),
3990 },
3991 Self::Est(est) => write!(f, "{est}"),
3992 Self::Text { text, slots } => {
3993 if slots.is_empty() {
3994 write!(f, "{text}")
3995 } else {
3996 // need to replace placeholders according to `slots`.
3997 // just find-and-replace wouldn't be safe/perfect, we
3998 // want to use the actual parser; right now we reuse
3999 // another implementation by just converting to EST and
4000 // printing that
4001 match self.est(fallback_est) {
4002 Ok(est) => write!(f, "{est}"),
4003 Err(e) => write!(f, "<invalid linked policy: {e}>"),
4004 }
4005 }
4006 }
4007 }
4008 }
4009}
4010
4011/// Expressions to be evaluated
4012#[repr(transparent)]
4013#[derive(Debug, Clone, RefCast)]
4014pub struct Expression(pub(crate) ast::Expr);
4015
4016#[doc(hidden)] // because this converts to a private/internal type
4017impl AsRef<ast::Expr> for Expression {
4018 fn as_ref(&self) -> &ast::Expr {
4019 &self.0
4020 }
4021}
4022
4023#[doc(hidden)]
4024impl From<ast::Expr> for Expression {
4025 fn from(expr: ast::Expr) -> Self {
4026 Self(expr)
4027 }
4028}
4029
4030impl Expression {
4031 /// Create an expression representing a literal string.
4032 pub fn new_string(value: String) -> Self {
4033 Self(ast::Expr::val(value))
4034 }
4035
4036 /// Create an expression representing a literal bool.
4037 pub fn new_bool(value: bool) -> Self {
4038 Self(ast::Expr::val(value))
4039 }
4040
4041 /// Create an expression representing a literal long.
4042 pub fn new_long(value: ast::Integer) -> Self {
4043 Self(ast::Expr::val(value))
4044 }
4045
4046 /// Create an expression representing a record.
4047 ///
4048 /// Error if any key appears two or more times in `fields`.
4049 pub fn new_record(
4050 fields: impl IntoIterator<Item = (String, Self)>,
4051 ) -> Result<Self, ExpressionConstructionError> {
4052 Ok(Self(ast::Expr::record(
4053 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4054 )?))
4055 }
4056
4057 /// Create an expression representing a Set.
4058 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4059 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
4060 }
4061
4062 /// Create an expression representing an ip address.
4063 /// This function does not perform error checking on the source string,
4064 /// it creates an expression that calls the `ip` constructor.
4065 pub fn new_ip(src: impl AsRef<str>) -> Self {
4066 let src_expr = ast::Expr::val(src.as_ref());
4067 Self(ast::Expr::call_extension_fn(
4068 ip_extension_name(),
4069 vec![src_expr],
4070 ))
4071 }
4072
4073 /// Create an expression representing a fixed precision decimal number.
4074 /// This function does not perform error checking on the source string,
4075 /// it creates an expression that calls the `decimal` constructor.
4076 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4077 let src_expr = ast::Expr::val(src.as_ref());
4078 Self(ast::Expr::call_extension_fn(
4079 decimal_extension_name(),
4080 vec![src_expr],
4081 ))
4082 }
4083
4084 /// Create an expression representing a particular instant of time.
4085 /// This function does not perform error checking on the source string,
4086 /// it creates an expression that calls the `datetime` constructor.
4087 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4088 let src_expr = ast::Expr::val(src.as_ref());
4089 Self(ast::Expr::call_extension_fn(
4090 datetime_extension_name(),
4091 vec![src_expr],
4092 ))
4093 }
4094
4095 /// Create an expression representing a duration of time.
4096 /// This function does not perform error checking on the source string,
4097 /// it creates an expression that calls the `datetime` constructor.
4098 pub fn new_duration(src: impl AsRef<str>) -> Self {
4099 let src_expr = ast::Expr::val(src.as_ref());
4100 Self(ast::Expr::call_extension_fn(
4101 duration_extension_name(),
4102 vec![src_expr],
4103 ))
4104 }
4105}
4106
4107#[cfg(test)]
4108impl Expression {
4109 /// Deconstruct an [`Expression`] to get the internal type.
4110 /// This function is only intended to be used internally.
4111 pub(crate) fn into_inner(self) -> ast::Expr {
4112 self.0
4113 }
4114}
4115
4116impl FromStr for Expression {
4117 type Err = ParseErrors;
4118
4119 /// create an Expression using Cedar syntax
4120 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4121 ast::Expr::from_str(expression)
4122 .map(Expression)
4123 .map_err(Into::into)
4124 }
4125}
4126
4127impl std::fmt::Display for Expression {
4128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4129 write!(f, "{}", &self.0)
4130 }
4131}
4132
4133/// "Restricted" expressions are used for attribute values and `context`.
4134///
4135/// Restricted expressions can contain only the following:
4136/// - bool, int, and string literals
4137/// - literal `EntityUid`s such as `User::"alice"`
4138/// - extension function calls, where the arguments must be other things
4139/// on this list
4140/// - set and record literals, where the values must be other things on
4141/// this list
4142///
4143/// That means the following are not allowed in restricted expressions:
4144/// - `principal`, `action`, `resource`, `context`
4145/// - builtin operators and functions, including `.`, `in`, `has`, `like`,
4146/// `.contains()`
4147/// - if-then-else expressions
4148#[repr(transparent)]
4149#[derive(Debug, Clone, RefCast, PartialEq, Eq)]
4150pub struct RestrictedExpression(pub(crate) ast::RestrictedExpr);
4151
4152#[doc(hidden)] // because this converts to a private/internal type
4153impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
4154 fn as_ref(&self) -> &ast::RestrictedExpr {
4155 &self.0
4156 }
4157}
4158
4159#[doc(hidden)]
4160impl From<ast::RestrictedExpr> for RestrictedExpression {
4161 fn from(expr: ast::RestrictedExpr) -> Self {
4162 Self(expr)
4163 }
4164}
4165
4166impl RestrictedExpression {
4167 /// Create an expression representing a literal string.
4168 pub fn new_string(value: String) -> Self {
4169 Self(ast::RestrictedExpr::val(value))
4170 }
4171
4172 /// Create an expression representing a literal bool.
4173 pub fn new_bool(value: bool) -> Self {
4174 Self(ast::RestrictedExpr::val(value))
4175 }
4176
4177 /// Create an expression representing a literal long.
4178 pub fn new_long(value: ast::Integer) -> Self {
4179 Self(ast::RestrictedExpr::val(value))
4180 }
4181
4182 /// Create an expression representing a literal `EntityUid`.
4183 pub fn new_entity_uid(value: EntityUid) -> Self {
4184 Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
4185 }
4186
4187 /// Create an expression representing a record.
4188 ///
4189 /// Error if any key appears two or more times in `fields`.
4190 pub fn new_record(
4191 fields: impl IntoIterator<Item = (String, Self)>,
4192 ) -> Result<Self, ExpressionConstructionError> {
4193 Ok(Self(ast::RestrictedExpr::record(
4194 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4195 )?))
4196 }
4197
4198 /// Create an expression representing a Set.
4199 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4200 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
4201 }
4202
4203 /// Create an expression representing an ip address.
4204 /// This function does not perform error checking on the source string,
4205 /// it creates an expression that calls the `ip` constructor.
4206 pub fn new_ip(src: impl AsRef<str>) -> Self {
4207 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4208 Self(ast::RestrictedExpr::call_extension_fn(
4209 ip_extension_name(),
4210 [src_expr],
4211 ))
4212 }
4213
4214 /// Create an expression representing a fixed precision decimal number.
4215 /// This function does not perform error checking on the source string,
4216 /// it creates an expression that calls the `decimal` constructor.
4217 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4218 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4219 Self(ast::RestrictedExpr::call_extension_fn(
4220 decimal_extension_name(),
4221 [src_expr],
4222 ))
4223 }
4224
4225 /// Create an expression representing a particular instant of time.
4226 /// This function does not perform error checking on the source string,
4227 /// it creates an expression that calls the `datetime` constructor.
4228 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4229 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4230 Self(ast::RestrictedExpr::call_extension_fn(
4231 datetime_extension_name(),
4232 [src_expr],
4233 ))
4234 }
4235
4236 /// Create an expression representing a duration of time.
4237 /// This function does not perform error checking on the source string,
4238 /// it creates an expression that calls the `datetime` constructor.
4239 pub fn new_duration(src: impl AsRef<str>) -> Self {
4240 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4241 Self(ast::RestrictedExpr::call_extension_fn(
4242 duration_extension_name(),
4243 [src_expr],
4244 ))
4245 }
4246
4247 /// Create an unknown expression
4248 #[cfg(feature = "partial-eval")]
4249 pub fn new_unknown(name: impl AsRef<str>) -> Self {
4250 Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
4251 name.as_ref(),
4252 )))
4253 }
4254}
4255
4256#[cfg(test)]
4257impl RestrictedExpression {
4258 /// Deconstruct an [`RestrictedExpression`] to get the internal type.
4259 /// This function is only intended to be used internally.
4260 pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
4261 self.0
4262 }
4263}
4264
4265fn decimal_extension_name() -> ast::Name {
4266 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4267 #[allow(clippy::unwrap_used)]
4268 ast::Name::unqualified_name("decimal".parse().unwrap())
4269}
4270
4271fn ip_extension_name() -> ast::Name {
4272 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4273 #[allow(clippy::unwrap_used)]
4274 ast::Name::unqualified_name("ip".parse().unwrap())
4275}
4276
4277fn datetime_extension_name() -> ast::Name {
4278 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4279 #[allow(clippy::unwrap_used)]
4280 ast::Name::unqualified_name("datetime".parse().unwrap())
4281}
4282
4283fn duration_extension_name() -> ast::Name {
4284 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4285 #[allow(clippy::unwrap_used)]
4286 ast::Name::unqualified_name("duration".parse().unwrap())
4287}
4288
4289impl FromStr for RestrictedExpression {
4290 type Err = RestrictedExpressionParseError;
4291
4292 /// create a `RestrictedExpression` using Cedar syntax
4293 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4294 ast::RestrictedExpr::from_str(expression)
4295 .map(RestrictedExpression)
4296 .map_err(Into::into)
4297 }
4298}
4299
4300/// Builder for a [`Request`]
4301///
4302/// The default for principal, action, resource, and context fields is Unknown
4303/// for partial evaluation.
4304#[doc = include_str!("../experimental_warning.md")]
4305#[cfg(feature = "partial-eval")]
4306#[derive(Debug, Clone)]
4307pub struct RequestBuilder<S> {
4308 principal: ast::EntityUIDEntry,
4309 action: ast::EntityUIDEntry,
4310 resource: ast::EntityUIDEntry,
4311 /// Here, `None` means unknown
4312 context: Option<ast::Context>,
4313 schema: S,
4314}
4315
4316/// A marker type that indicates [`Schema`] is not set for a request
4317#[doc = include_str!("../experimental_warning.md")]
4318#[cfg(feature = "partial-eval")]
4319#[derive(Debug, Clone, Copy)]
4320pub struct UnsetSchema;
4321
4322#[cfg(feature = "partial-eval")]
4323impl Default for RequestBuilder<UnsetSchema> {
4324 fn default() -> Self {
4325 Self {
4326 principal: ast::EntityUIDEntry::unknown(),
4327 action: ast::EntityUIDEntry::unknown(),
4328 resource: ast::EntityUIDEntry::unknown(),
4329 context: None,
4330 schema: UnsetSchema,
4331 }
4332 }
4333}
4334
4335#[cfg(feature = "partial-eval")]
4336impl<S> RequestBuilder<S> {
4337 /// Set the principal.
4338 ///
4339 /// Note that you can create the `EntityUid` using `.parse()` on any
4340 /// string (via the `FromStr` implementation for `EntityUid`).
4341 #[must_use]
4342 pub fn principal(self, principal: EntityUid) -> Self {
4343 Self {
4344 principal: ast::EntityUIDEntry::known(principal.into(), None),
4345 ..self
4346 }
4347 }
4348
4349 /// Set the principal to be unknown, but known to belong to a certain entity type.
4350 ///
4351 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4352 #[must_use]
4353 pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
4354 Self {
4355 principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
4356 ..self
4357 }
4358 }
4359
4360 /// Set the action.
4361 ///
4362 /// Note that you can create the `EntityUid` using `.parse()` on any
4363 /// string (via the `FromStr` implementation for `EntityUid`).
4364 #[must_use]
4365 pub fn action(self, action: EntityUid) -> Self {
4366 Self {
4367 action: ast::EntityUIDEntry::known(action.into(), None),
4368 ..self
4369 }
4370 }
4371
4372 /// Set the resource.
4373 ///
4374 /// Note that you can create the `EntityUid` using `.parse()` on any
4375 /// string (via the `FromStr` implementation for `EntityUid`).
4376 #[must_use]
4377 pub fn resource(self, resource: EntityUid) -> Self {
4378 Self {
4379 resource: ast::EntityUIDEntry::known(resource.into(), None),
4380 ..self
4381 }
4382 }
4383
4384 /// Set the resource to be unknown, but known to belong to a certain entity type.
4385 ///
4386 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4387 #[must_use]
4388 pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4389 Self {
4390 resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4391 ..self
4392 }
4393 }
4394
4395 /// Set the context.
4396 #[must_use]
4397 pub fn context(self, context: Context) -> Self {
4398 Self {
4399 context: Some(context.0),
4400 ..self
4401 }
4402 }
4403}
4404
4405#[cfg(feature = "partial-eval")]
4406impl RequestBuilder<UnsetSchema> {
4407 /// Set the schema. If present, this will be used for request validation.
4408 #[must_use]
4409 pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4410 RequestBuilder {
4411 principal: self.principal,
4412 action: self.action,
4413 resource: self.resource,
4414 context: self.context,
4415 schema,
4416 }
4417 }
4418
4419 /// Create the [`Request`]
4420 pub fn build(self) -> Request {
4421 Request(ast::Request::new_unchecked(
4422 self.principal,
4423 self.action,
4424 self.resource,
4425 self.context,
4426 ))
4427 }
4428}
4429
4430#[cfg(feature = "partial-eval")]
4431impl RequestBuilder<&Schema> {
4432 /// Create the [`Request`]
4433 pub fn build(self) -> Result<Request, RequestValidationError> {
4434 Ok(Request(ast::Request::new_with_unknowns(
4435 self.principal,
4436 self.action,
4437 self.resource,
4438 self.context,
4439 Some(&self.schema.0),
4440 Extensions::all_available(),
4441 )?))
4442 }
4443}
4444
4445/// An authorization request is a tuple `<P, A, R, C>` where
4446/// * P is the principal [`EntityUid`],
4447/// * A is the action [`EntityUid`],
4448/// * R is the resource [`EntityUid`], and
4449/// * C is the request [`Context`] record.
4450///
4451/// It represents an authorization request asking the question, "Can this
4452/// principal take this action on this resource in this context?"
4453#[repr(transparent)]
4454#[derive(Debug, Clone, RefCast)]
4455pub struct Request(pub(crate) ast::Request);
4456
4457#[doc(hidden)] // because this converts to a private/internal type
4458impl AsRef<ast::Request> for Request {
4459 fn as_ref(&self) -> &ast::Request {
4460 &self.0
4461 }
4462}
4463
4464#[doc(hidden)]
4465impl From<ast::Request> for Request {
4466 fn from(req: ast::Request) -> Self {
4467 Self(req)
4468 }
4469}
4470
4471impl Request {
4472 /// Create a [`RequestBuilder`]
4473 #[doc = include_str!("../experimental_warning.md")]
4474 #[cfg(feature = "partial-eval")]
4475 pub fn builder() -> RequestBuilder<UnsetSchema> {
4476 RequestBuilder::default()
4477 }
4478
4479 /// Create a Request.
4480 ///
4481 /// Note that you can create the `EntityUid`s using `.parse()` on any
4482 /// string (via the `FromStr` implementation for `EntityUid`).
4483 /// The principal, action, and resource fields are optional to support
4484 /// the case where these fields do not contribute to authorization
4485 /// decisions (e.g., because they are not used in your policies).
4486 /// If any of the fields are `None`, we will automatically generate
4487 /// a unique entity UID that is not equal to any UID in the store.
4488 ///
4489 /// If `schema` is present, this constructor will validate that the
4490 /// `Request` complies with the given `schema`.
4491 pub fn new(
4492 principal: EntityUid,
4493 action: EntityUid,
4494 resource: EntityUid,
4495 context: Context,
4496 schema: Option<&Schema>,
4497 ) -> Result<Self, RequestValidationError> {
4498 Ok(Self(ast::Request::new(
4499 (principal.into(), None),
4500 (action.into(), None),
4501 (resource.into(), None),
4502 context.0,
4503 schema.map(|schema| &schema.0),
4504 Extensions::all_available(),
4505 )?))
4506 }
4507
4508 /// Get the context component of the request. Returns `None` if the context is
4509 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4510 pub fn context(&self) -> Option<&Context> {
4511 self.0.context().map(Context::ref_cast)
4512 }
4513
4514 /// Get the principal component of the request. Returns `None` if the principal is
4515 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4516 pub fn principal(&self) -> Option<&EntityUid> {
4517 match self.0.principal() {
4518 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4519 ast::EntityUIDEntry::Unknown { .. } => None,
4520 }
4521 }
4522
4523 /// Get the action component of the request. Returns `None` if the action is
4524 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4525 pub fn action(&self) -> Option<&EntityUid> {
4526 match self.0.action() {
4527 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4528 ast::EntityUIDEntry::Unknown { .. } => None,
4529 }
4530 }
4531
4532 /// Get the resource component of the request. Returns `None` if the resource is
4533 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4534 pub fn resource(&self) -> Option<&EntityUid> {
4535 match self.0.resource() {
4536 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4537 ast::EntityUIDEntry::Unknown { .. } => None,
4538 }
4539 }
4540}
4541
4542/// the Context object for an authorization request
4543#[repr(transparent)]
4544#[derive(Debug, Clone, RefCast)]
4545pub struct Context(ast::Context);
4546
4547#[doc(hidden)] // because this converts to a private/internal type
4548impl AsRef<ast::Context> for Context {
4549 fn as_ref(&self) -> &ast::Context {
4550 &self.0
4551 }
4552}
4553
4554impl Context {
4555 /// Create an empty `Context`
4556 /// ```
4557 /// # use cedar_policy::Context;
4558 /// let context = Context::empty();
4559 /// # assert_eq!(context.into_iter().next(), None);
4560 /// ```
4561 pub fn empty() -> Self {
4562 Self(ast::Context::empty())
4563 }
4564
4565 /// Create a `Context` from a map of key to "restricted expression",
4566 /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
4567 /// of `(key, restricted expression)` pairs.
4568 /// ```
4569 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4570 /// # use std::str::FromStr;
4571 /// let context = Context::from_pairs([
4572 /// ("key".to_string(), RestrictedExpression::from_str(r#""value""#).unwrap()),
4573 /// ("age".to_string(), RestrictedExpression::from_str("18").unwrap()),
4574 /// ]).unwrap();
4575 /// # // create a request
4576 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4577 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4578 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4579 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4580 /// ```
4581 pub fn from_pairs(
4582 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4583 ) -> Result<Self, ContextCreationError> {
4584 Ok(Self(ast::Context::from_pairs(
4585 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4586 Extensions::all_available(),
4587 )?))
4588 }
4589
4590 /// Retrieves a value from the Context by its key.
4591 ///
4592 /// # Arguments
4593 ///
4594 /// * `key` - The key to look up in the context
4595 ///
4596 /// # Returns
4597 ///
4598 /// * `Some(EvalResult)` - If the key exists in the context, returns its value
4599 /// * `None` - If the key doesn't exist or if the context is not a Value type
4600 ///
4601 /// # Examples
4602 ///
4603 /// ```
4604 /// # use cedar_policy::{Context, Request, EntityUid};
4605 /// # use std::str::FromStr;
4606 /// let context = Context::from_json_str(r#"{"rayId": "abc123"}"#, None).unwrap();
4607 /// if let Some(value) = context.get("rayId") {
4608 /// // value here is an EvalResult, convertible from the internal Value type
4609 /// println!("Found value: {:?}", value);
4610 /// }
4611 /// assert_eq!(context.get("nonexistent"), None);
4612 /// ```
4613 pub fn get(&self, key: &str) -> Option<EvalResult> {
4614 match &self.0 {
4615 ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4616 ast::Context::RestrictedResidual(_) => None,
4617 }
4618 }
4619
4620 /// Create a `Context` from a string containing JSON (which must be a JSON
4621 /// object, not any other JSON type, or you will get an error here).
4622 /// JSON here must use the `__entity` and `__extn` escapes for entity
4623 /// references, extension values, etc.
4624 ///
4625 /// If a `schema` is provided, this will inform the parsing: for instance, it
4626 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4627 /// if attributes have the wrong types (e.g., string instead of integer).
4628 /// Since different Actions have different schemas for `Context`, you also
4629 /// must specify the `Action` for schema-based parsing.
4630 /// ```
4631 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4632 /// # use std::str::FromStr;
4633 /// let json_data = r#"{
4634 /// "sub": "1234",
4635 /// "groups": {
4636 /// "1234": {
4637 /// "group_id": "abcd",
4638 /// "group_name": "test-group"
4639 /// }
4640 /// }
4641 /// }"#;
4642 /// let context = Context::from_json_str(json_data, None).unwrap();
4643 /// # // create a request
4644 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4645 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4646 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4647 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4648 /// ```
4649 pub fn from_json_str(
4650 json: &str,
4651 schema: Option<(&Schema, &EntityUid)>,
4652 ) -> Result<Self, ContextJsonError> {
4653 let schema = schema
4654 .map(|(s, uid)| Self::get_context_schema(s, uid))
4655 .transpose()?;
4656 let context = cedar_policy_core::entities::ContextJsonParser::new(
4657 schema.as_ref(),
4658 Extensions::all_available(),
4659 )
4660 .from_json_str(json)?;
4661 Ok(Self(context))
4662 }
4663
4664 /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
4665 /// not any other JSON type, or you will get an error here).
4666 /// JSON here must use the `__entity` and `__extn` escapes for entity
4667 /// references, extension values, etc.
4668 ///
4669 /// If a `schema` is provided, this will inform the parsing: for instance, it
4670 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4671 /// if attributes have the wrong types (e.g., string instead of integer).
4672 /// Since different Actions have different schemas for `Context`, you also
4673 /// must specify the `Action` for schema-based parsing.
4674 /// ```
4675 /// # use cedar_policy::{Context, EntityUid, EntityId, EntityTypeName, RestrictedExpression, Request, Schema};
4676 /// # use std::str::FromStr;
4677 /// let schema_json = serde_json::json!(
4678 /// {
4679 /// "": {
4680 /// "entityTypes": {
4681 /// "User": {},
4682 /// "Album": {},
4683 /// },
4684 /// "actions": {
4685 /// "view": {
4686 /// "appliesTo": {
4687 /// "principalTypes": ["User"],
4688 /// "resourceTypes": ["Album"],
4689 /// "context": {
4690 /// "type": "Record",
4691 /// "attributes": {
4692 /// "sub": { "type": "Long" }
4693 /// }
4694 /// }
4695 /// }
4696 /// }
4697 /// }
4698 /// }
4699 /// });
4700 /// let schema = Schema::from_json_value(schema_json).unwrap();
4701 ///
4702 /// let a_eid = EntityId::from_str("view").unwrap();
4703 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4704 /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
4705 /// let data = serde_json::json!({
4706 /// "sub": 1234
4707 /// });
4708 /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
4709 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4710 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4711 /// # let request: Request = Request::new(p, action, r, context, Some(&schema)).unwrap();
4712 /// ```
4713 pub fn from_json_value(
4714 json: serde_json::Value,
4715 schema: Option<(&Schema, &EntityUid)>,
4716 ) -> Result<Self, ContextJsonError> {
4717 let schema = schema
4718 .map(|(s, uid)| Self::get_context_schema(s, uid))
4719 .transpose()?;
4720 let context = cedar_policy_core::entities::ContextJsonParser::new(
4721 schema.as_ref(),
4722 Extensions::all_available(),
4723 )
4724 .from_json_value(json)?;
4725 Ok(Self(context))
4726 }
4727
4728 /// Create a `Context` from a JSON file. The JSON file must contain a JSON
4729 /// object, not any other JSON type, or you will get an error here.
4730 /// JSON here must use the `__entity` and `__extn` escapes for entity
4731 /// references, extension values, etc.
4732 ///
4733 /// If a `schema` is provided, this will inform the parsing: for instance, it
4734 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4735 /// if attributes have the wrong types (e.g., string instead of integer).
4736 /// Since different Actions have different schemas for `Context`, you also
4737 /// must specify the `Action` for schema-based parsing.
4738 /// ```no_run
4739 /// # use cedar_policy::{Context, RestrictedExpression};
4740 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
4741 /// # use std::collections::HashMap;
4742 /// # use std::str::FromStr;
4743 /// # use std::fs::File;
4744 /// let mut json = File::open("json_file.json").unwrap();
4745 /// let context = Context::from_json_file(&json, None).unwrap();
4746 /// # // create a request
4747 /// # let p_eid = EntityId::from_str("alice").unwrap();
4748 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
4749 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
4750 /// #
4751 /// # let a_eid = EntityId::from_str("view").unwrap();
4752 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4753 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
4754 /// # let r_eid = EntityId::from_str("trip").unwrap();
4755 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
4756 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
4757 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4758 /// ```
4759 pub fn from_json_file(
4760 json: impl std::io::Read,
4761 schema: Option<(&Schema, &EntityUid)>,
4762 ) -> Result<Self, ContextJsonError> {
4763 let schema = schema
4764 .map(|(s, uid)| Self::get_context_schema(s, uid))
4765 .transpose()?;
4766 let context = cedar_policy_core::entities::ContextJsonParser::new(
4767 schema.as_ref(),
4768 Extensions::all_available(),
4769 )
4770 .from_json_file(json)?;
4771 Ok(Self(context))
4772 }
4773
4774 /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
4775 fn get_context_schema(
4776 schema: &Schema,
4777 action: &EntityUid,
4778 ) -> Result<impl ContextSchema, ContextJsonError> {
4779 cedar_policy_core::validator::context_schema_for_action(&schema.0, action.as_ref())
4780 .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4781 }
4782
4783 /// Merge this [`Context`] with another context (or iterator over
4784 /// `(String, RestrictedExpression)` pairs), returning an error if the two
4785 /// contain overlapping keys
4786 pub fn merge(
4787 self,
4788 other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
4789 ) -> Result<Self, ContextCreationError> {
4790 Self::from_pairs(self.into_iter().chain(other_context))
4791 }
4792
4793 /// Validates this context against the provided schema
4794 ///
4795 /// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
4796 ///
4797 /// This validation is already handled by `Request::new`, so there is no need to separately call
4798 /// if you are validating the whole request
4799 pub fn validate(
4800 &self,
4801 schema: &crate::Schema,
4802 action: &EntityUid,
4803 ) -> std::result::Result<(), RequestValidationError> {
4804 // Call the validate_context function from coreschema.rs
4805 Ok(RequestSchema::validate_context(
4806 &schema.0,
4807 &self.0,
4808 action.as_ref(),
4809 Extensions::all_available(),
4810 )?)
4811 }
4812}
4813
4814/// Utilities for implementing `IntoIterator` for `Context`
4815mod context {
4816 use super::{ast, RestrictedExpression};
4817
4818 /// `IntoIter` iterator for `Context`
4819 #[derive(Debug)]
4820 pub struct IntoIter {
4821 pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
4822 }
4823
4824 impl Iterator for IntoIter {
4825 type Item = (String, RestrictedExpression);
4826
4827 fn next(&mut self) -> Option<Self::Item> {
4828 self.inner
4829 .next()
4830 .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
4831 }
4832 }
4833}
4834
4835impl IntoIterator for Context {
4836 type Item = (String, RestrictedExpression);
4837
4838 type IntoIter = context::IntoIter;
4839
4840 fn into_iter(self) -> Self::IntoIter {
4841 Self::IntoIter {
4842 inner: self.0.into_iter(),
4843 }
4844 }
4845}
4846
4847#[doc(hidden)]
4848impl From<ast::Context> for Context {
4849 fn from(c: ast::Context) -> Self {
4850 Self(c)
4851 }
4852}
4853
4854impl std::fmt::Display for Request {
4855 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4856 write!(f, "{}", self.0)
4857 }
4858}
4859
4860impl std::fmt::Display for Context {
4861 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4862 write!(f, "{}", self.0)
4863 }
4864}
4865
4866/// Result of Evaluation
4867#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4868pub enum EvalResult {
4869 /// Boolean value
4870 Bool(bool),
4871 /// Signed integer value
4872 Long(ast::Integer),
4873 /// String value
4874 String(String),
4875 /// Entity Uid
4876 EntityUid(EntityUid),
4877 /// A first-class set
4878 Set(Set),
4879 /// A first-class anonymous record
4880 Record(Record),
4881 /// An extension value, currently limited to String results
4882 ExtensionValue(String),
4883 // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
4884}
4885
4886/// Sets of Cedar values
4887#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4888pub struct Set(BTreeSet<EvalResult>);
4889
4890impl Set {
4891 /// Iterate over the members of the set
4892 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
4893 self.0.iter()
4894 }
4895
4896 /// Is a given element in the set
4897 pub fn contains(&self, elem: &EvalResult) -> bool {
4898 self.0.contains(elem)
4899 }
4900
4901 /// Get the number of members of the set
4902 pub fn len(&self) -> usize {
4903 self.0.len()
4904 }
4905
4906 /// Test if the set is empty
4907 pub fn is_empty(&self) -> bool {
4908 self.0.is_empty()
4909 }
4910}
4911
4912/// A record of Cedar values
4913#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4914pub struct Record(BTreeMap<String, EvalResult>);
4915
4916impl Record {
4917 /// Iterate over the attribute/value pairs in the record
4918 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
4919 self.0.iter()
4920 }
4921
4922 /// Check if a given attribute is in the record
4923 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
4924 self.0.contains_key(key.as_ref())
4925 }
4926
4927 /// Get a given attribute from the record
4928 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
4929 self.0.get(key.as_ref())
4930 }
4931
4932 /// Get the number of attributes in the record
4933 pub fn len(&self) -> usize {
4934 self.0.len()
4935 }
4936
4937 /// Test if the record is empty
4938 pub fn is_empty(&self) -> bool {
4939 self.0.is_empty()
4940 }
4941}
4942
4943#[doc(hidden)]
4944impl From<ast::Value> for EvalResult {
4945 fn from(v: ast::Value) -> Self {
4946 match v.value {
4947 ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
4948 ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
4949 ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
4950 ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
4951 Self::EntityUid(ast::EntityUID::clone(&e).into())
4952 }
4953 ast::ValueKind::Set(set) => Self::Set(Set(set
4954 .authoritative
4955 .iter()
4956 .map(|v| v.clone().into())
4957 .collect())),
4958 ast::ValueKind::Record(record) => Self::Record(Record(
4959 record
4960 .iter()
4961 .map(|(k, v)| (k.to_string(), v.clone().into()))
4962 .collect(),
4963 )),
4964 ast::ValueKind::ExtensionValue(ev) => {
4965 Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
4966 }
4967 }
4968 }
4969}
4970
4971#[doc(hidden)]
4972// PANIC SAFETY: see the panic safety comments below
4973#[allow(clippy::fallible_impl_from)]
4974impl From<EvalResult> for Expression {
4975 fn from(res: EvalResult) -> Self {
4976 match res {
4977 EvalResult::Bool(b) => Self::new_bool(b),
4978 EvalResult::Long(l) => Self::new_long(l),
4979 EvalResult::String(s) => Self::new_string(s),
4980 EvalResult::EntityUid(eid) => {
4981 Self::from(ast::Expr::from(ast::Value::from(ast::EntityUID::from(eid))))
4982 }
4983 EvalResult::Set(set) => Self::new_set(set.iter().cloned().map(Self::from)),
4984 EvalResult::Record(r) => {
4985 // PANIC SAFETY: record originates from EvalResult so should not panic when reconstructing as an Expression
4986 #[allow(clippy::unwrap_used)]
4987 Self::new_record(r.iter().map(|(k, v)| (k.clone(), Self::from(v.clone())))).unwrap()
4988 }
4989 EvalResult::ExtensionValue(s) => {
4990 // PANIC SAFETY: the string s is constructed using RestrictedExpr::to_string() so should not panic when being parsed back into a RestrictedExpr
4991 #[allow(clippy::unwrap_used)]
4992 let expr: ast::Expr = ast::RestrictedExpr::from_str(&s).unwrap().into();
4993 Self::from(expr)
4994 }
4995 }
4996 }
4997}
4998
4999impl std::fmt::Display for EvalResult {
5000 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5001 match self {
5002 Self::Bool(b) => write!(f, "{b}"),
5003 Self::Long(l) => write!(f, "{l}"),
5004 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
5005 Self::EntityUid(uid) => write!(f, "{uid}"),
5006 Self::Set(s) => {
5007 write!(f, "[")?;
5008 for (i, ev) in s.iter().enumerate() {
5009 write!(f, "{ev}")?;
5010 if (i + 1) < s.len() {
5011 write!(f, ", ")?;
5012 }
5013 }
5014 write!(f, "]")?;
5015 Ok(())
5016 }
5017 Self::Record(r) => {
5018 write!(f, "{{")?;
5019 for (i, (k, v)) in r.iter().enumerate() {
5020 write!(f, "\"{}\": {v}", k.escape_debug())?;
5021 if (i + 1) < r.len() {
5022 write!(f, ", ")?;
5023 }
5024 }
5025 write!(f, "}}")?;
5026 Ok(())
5027 }
5028 Self::ExtensionValue(s) => write!(f, "{s}"),
5029 }
5030 }
5031}
5032
5033/// Evaluates an expression.
5034///
5035/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
5036/// passing the wrong number of arguments to a function etc.), that error is returned as a String
5037pub fn eval_expression(
5038 request: &Request,
5039 entities: &Entities,
5040 expr: &Expression,
5041) -> Result<EvalResult, EvaluationError> {
5042 let all_ext = Extensions::all_available();
5043 let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
5044 Ok(EvalResult::from(
5045 // Evaluate under the empty slot map, as an expression should not have slots
5046 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
5047 ))
5048}
5049
5050#[cfg(feature = "tpe")]
5051pub use tpe::*;
5052
5053#[cfg(feature = "tpe")]
5054mod tpe {
5055 use std::collections::{BTreeMap, HashMap, HashSet};
5056 use std::sync::Arc;
5057
5058 use cedar_policy_core::ast::{self, Value};
5059 use cedar_policy_core::authorizer::Decision;
5060 use cedar_policy_core::batched_evaluator::is_authorized_batched;
5061 use cedar_policy_core::batched_evaluator::{
5062 err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
5063 };
5064 use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
5065 use cedar_policy_core::extensions::Extensions;
5066 use cedar_policy_core::tpe;
5067 use itertools::Itertools;
5068 use ref_cast::RefCast;
5069 use smol_str::SmolStr;
5070
5071 use crate::{
5072 api, tpe_err, Authorizer, Context, Entities, EntityId, EntityTypeName, EntityUid,
5073 PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicySet,
5074 Request, RequestValidationError, RestrictedExpression, Schema,
5075 };
5076 use crate::{Entity, TpeReauthorizationError};
5077
5078 /// A partial [`EntityUid`].
5079 /// That is, its [`EntityId`] could be unknown
5080 #[repr(transparent)]
5081 #[derive(Debug, Clone, RefCast)]
5082 pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
5083
5084 #[doc(hidden)]
5085 impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
5086 fn as_ref(&self) -> &tpe::request::PartialEntityUID {
5087 &self.0
5088 }
5089 }
5090
5091 impl PartialEntityUid {
5092 /// Construct a [`PartialEntityUid`]
5093 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
5094 Self(tpe::request::PartialEntityUID {
5095 ty: ty.0,
5096 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
5097 })
5098 }
5099
5100 /// Construct a [`PartialEntityUid`] from a concrete [`EntityUid`].
5101 pub fn from_concrete(euid: EntityUid) -> Self {
5102 let (ty, eid) = euid.0.components();
5103 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
5104 }
5105 }
5106
5107 /// A partial [`Request`]
5108 /// Its principal/resource types and action must be known and its context
5109 /// must either be fully known or unknown
5110 #[repr(transparent)]
5111 #[derive(Debug, Clone, RefCast)]
5112 pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
5113
5114 #[doc(hidden)]
5115 impl AsRef<tpe::request::PartialRequest> for PartialRequest {
5116 fn as_ref(&self) -> &tpe::request::PartialRequest {
5117 &self.0
5118 }
5119 }
5120
5121 impl PartialRequest {
5122 /// Construct a valid [`PartialRequest`] according to a [`Schema`]
5123 pub fn new(
5124 principal: PartialEntityUid,
5125 action: EntityUid,
5126 resource: PartialEntityUid,
5127 context: Option<Context>,
5128 schema: &Schema,
5129 ) -> Result<Self, PartialRequestCreationError> {
5130 let context = context
5131 .map(|c| match c.0 {
5132 ast::Context::RestrictedResidual(_) => {
5133 Err(PartialRequestCreationError::ContextContainsUnknowns)
5134 }
5135 ast::Context::Value(m) => Ok(m),
5136 })
5137 .transpose()?;
5138 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
5139 .map(Self)
5140 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
5141 }
5142 }
5143
5144 /// Like [`PartialRequest`] but only `resource` can be unknown
5145 #[repr(transparent)]
5146 #[derive(Debug, Clone, RefCast)]
5147 pub struct ResourceQueryRequest(pub(crate) PartialRequest);
5148
5149 impl ResourceQueryRequest {
5150 /// Construct a valid [`ResourceQueryRequest`] according to a [`Schema`]
5151 pub fn new(
5152 principal: EntityUid,
5153 action: EntityUid,
5154 resource: EntityTypeName,
5155 context: Context,
5156 schema: &Schema,
5157 ) -> Result<Self, PartialRequestCreationError> {
5158 PartialRequest::new(
5159 PartialEntityUid(principal.0.into()),
5160 action,
5161 PartialEntityUid::new(resource, None),
5162 Some(context),
5163 schema,
5164 )
5165 .map(Self)
5166 }
5167
5168 /// Convert [`ResourceQueryRequest`] to a [`Request`] by providing the resource [`EntityId`]
5169 pub fn to_request(
5170 &self,
5171 resource_id: EntityId,
5172 schema: Option<&Schema>,
5173 ) -> Result<Request, RequestValidationError> {
5174 // PANIC SAFETY: various fields are validated through the constructor
5175 #[allow(clippy::unwrap_used)]
5176 Request::new(
5177 EntityUid(self.0 .0.get_principal().try_into().unwrap()),
5178 EntityUid(self.0 .0.get_action()),
5179 EntityUid::from_type_name_and_id(
5180 EntityTypeName(self.0 .0.get_resource_type()),
5181 resource_id,
5182 ),
5183 Context::from_pairs(
5184 self.0
5185 .0
5186 .get_context_attrs()
5187 .unwrap()
5188 .iter()
5189 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5190 )
5191 .unwrap(),
5192 schema,
5193 )
5194 }
5195 }
5196
5197 /// Like [`PartialRequest`] but only `principal` can be unknown
5198 #[repr(transparent)]
5199 #[derive(Debug, Clone, RefCast)]
5200 pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
5201
5202 impl PrincipalQueryRequest {
5203 /// Construct a valid [`PrincipalQueryRequest`] according to a [`Schema`]
5204 pub fn new(
5205 principal: EntityTypeName,
5206 action: EntityUid,
5207 resource: EntityUid,
5208 context: Context,
5209 schema: &Schema,
5210 ) -> Result<Self, PartialRequestCreationError> {
5211 PartialRequest::new(
5212 PartialEntityUid::new(principal, None),
5213 action,
5214 PartialEntityUid(resource.0.into()),
5215 Some(context),
5216 schema,
5217 )
5218 .map(Self)
5219 }
5220
5221 /// Convert [`PrincipalQueryRequest`] to a [`Request`] by providing the principal [`EntityId`]
5222 pub fn to_request(
5223 &self,
5224 principal_id: EntityId,
5225 schema: Option<&Schema>,
5226 ) -> Result<Request, RequestValidationError> {
5227 // PANIC SAFETY: various fields are validated through the constructor
5228 #[allow(clippy::unwrap_used)]
5229 Request::new(
5230 EntityUid::from_type_name_and_id(
5231 EntityTypeName(self.0 .0.get_principal_type()),
5232 principal_id,
5233 ),
5234 EntityUid(self.0 .0.get_action()),
5235 EntityUid(self.0 .0.get_resource().try_into().unwrap()),
5236 Context::from_pairs(
5237 self.0
5238 .0
5239 .get_context_attrs()
5240 .unwrap()
5241 .iter()
5242 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5243 )
5244 .unwrap(),
5245 schema,
5246 )
5247 }
5248 }
5249
5250 /// Defines a [`PartialRequest`] which additionally leaves the action
5251 /// undefined, enabling queries listing what actions might be authorized.
5252 ///
5253 /// See [`PolicySet::query_action`] for documentation and example usage.
5254 #[derive(Debug, Clone)]
5255 pub struct ActionQueryRequest {
5256 principal: PartialEntityUid,
5257 resource: PartialEntityUid,
5258 context: Option<Arc<BTreeMap<SmolStr, Value>>>,
5259 schema: Schema,
5260 }
5261
5262 impl ActionQueryRequest {
5263 /// Construct a valid [`ActionQueryRequest`] according to a [`Schema`]
5264 pub fn new(
5265 principal: PartialEntityUid,
5266 resource: PartialEntityUid,
5267 context: Option<Context>,
5268 schema: Schema,
5269 ) -> Result<Self, PartialRequestCreationError> {
5270 let context = context
5271 .map(|c| match c.0 {
5272 ast::Context::RestrictedResidual(_) => {
5273 Err(PartialRequestCreationError::ContextContainsUnknowns)
5274 }
5275 ast::Context::Value(m) => Ok(m),
5276 })
5277 .transpose()?;
5278 Ok(Self {
5279 principal,
5280 resource,
5281 context,
5282 schema,
5283 })
5284 }
5285
5286 fn partial_request(
5287 &self,
5288 action: EntityUid,
5289 ) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
5290 tpe::request::PartialRequest::new(
5291 self.principal.0.clone(),
5292 action.0,
5293 self.resource.0.clone(),
5294 self.context.clone(),
5295 &self.schema.0,
5296 )
5297 .map(PartialRequest)
5298 }
5299 }
5300
5301 /// Partial [`Entity`]
5302 #[repr(transparent)]
5303 #[derive(Debug, Clone, RefCast)]
5304 pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
5305
5306 impl PartialEntity {
5307 /// Construct a [`PartialEntity`]
5308 pub fn new(
5309 uid: EntityUid,
5310 attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5311 ancestors: Option<HashSet<EntityUid>>,
5312 tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
5313 schema: &Schema,
5314 ) -> Result<Self, PartialEntityError> {
5315 Ok(Self(tpe::entities::PartialEntity::new(
5316 uid.0,
5317 attrs
5318 .map(|ps| {
5319 ps.into_iter()
5320 .map(|(k, v)| {
5321 Ok((
5322 k,
5323 RestrictedEvaluator::new(Extensions::all_available())
5324 .interpret(v.0.as_borrowed())?,
5325 ))
5326 })
5327 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5328 })
5329 .transpose()?,
5330 ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
5331 tags.map(|ps| {
5332 ps.into_iter()
5333 .map(|(k, v)| {
5334 Ok((
5335 k,
5336 RestrictedEvaluator::new(Extensions::all_available())
5337 .interpret(v.0.as_borrowed())?,
5338 ))
5339 })
5340 .collect::<Result<BTreeMap<_, _>, EvaluationError>>()
5341 })
5342 .transpose()?,
5343 &schema.0,
5344 )?))
5345 }
5346 }
5347
5348 /// Partial [`Entities`]
5349 #[repr(transparent)]
5350 #[derive(Debug, Clone, RefCast)]
5351 pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
5352
5353 #[doc(hidden)]
5354 impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
5355 fn as_ref(&self) -> &tpe::entities::PartialEntities {
5356 &self.0
5357 }
5358 }
5359
5360 impl PartialEntities {
5361 /// Construct [`PartialEntities`] from a JSON value
5362 /// The `parent`, `attrs`, `tags` field must be either fully known or
5363 /// unknown. And parent entities cannot have unknown parents.
5364 pub fn from_json_value(
5365 value: serde_json::Value,
5366 schema: &Schema,
5367 ) -> Result<Self, tpe_err::EntitiesError> {
5368 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
5369 }
5370
5371 /// Construct [`PartialEntities`] given a fully concrete [`Entities`]
5372 pub fn from_concrete(
5373 entities: Entities,
5374 schema: &Schema,
5375 ) -> Result<Self, tpe_err::EntitiesError> {
5376 tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
5377 }
5378
5379 /// Create a `PartialEntities` with no entities
5380 pub fn empty() -> Self {
5381 Self(tpe::entities::PartialEntities::new())
5382 }
5383
5384 /// Construct [`PartialEntities`] from an iterator of [`PartialEntity`]
5385 pub fn from_partial_entities(
5386 entities: impl IntoIterator<Item = PartialEntity>,
5387 schema: &Schema,
5388 ) -> Result<Self, tpe_err::EntitiesError> {
5389 Ok(Self(tpe::entities::PartialEntities::from_entities(
5390 entities.into_iter().map(|entity| entity.0),
5391 &schema.0,
5392 )?))
5393 }
5394 }
5395
5396 /// A partial version of [`crate::Response`].
5397 #[repr(transparent)]
5398 #[derive(Debug, Clone, RefCast)]
5399 pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
5400
5401 #[doc(hidden)]
5402 impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
5403 fn as_ref(&self) -> &tpe::response::Response<'a> {
5404 &self.0
5405 }
5406 }
5407
5408 impl TpeResponse<'_> {
5409 /// Attempt to get the authorization decision
5410 pub fn decision(&self) -> Option<Decision> {
5411 self.0.decision()
5412 }
5413
5414 /// Perform reauthorization
5415 pub fn reauthorize(
5416 &self,
5417 request: &Request,
5418 entities: &Entities,
5419 ) -> Result<api::Response, TpeReauthorizationError> {
5420 self.0
5421 .reauthorize(&request.0, &entities.0)
5422 .map(Into::into)
5423 .map_err(Into::into)
5424 }
5425
5426 /// Return residuals as [`Policy`]s
5427 /// A [`Policy`] returned inherits [`crate::PolicyId`] and annotations from
5428 /// the corresponding input policy
5429 /// Its scope is unconstrained and its condition is in the form of a
5430 /// single `when` clause with the residual as the expression
5431 /// Use [`TpeResponse::nontrivial_residual_policies`] to get non-trivial residual policies
5432 pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
5433 self.0
5434 .residual_policies()
5435 .map(|p| Policy::from_ast(p.clone().into()))
5436 }
5437
5438 /// Returns an iterator of non-trivial (meaning more than just `true`
5439 /// or `false`) residuals as [`Policy`]s
5440 /// A [`Policy`] returned inherits [`crate::PolicyId`] and annotations from
5441 /// the corresponding input policy
5442 /// Its scope is unconstrained and its condition is in the form of a
5443 /// single `when` clause with the residual as the expression
5444 pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
5445 self.0
5446 .residual_permits()
5447 .chain(self.0.residual_forbids())
5448 .map(|p| Policy::from_ast(p.clone().into()))
5449 }
5450 }
5451
5452 /// Entity loader trait for batched evaluation.
5453 ///
5454 /// Loads entities on demand, returning `None` for missing entities.
5455 /// The `load_entities` function must load all requested entities,
5456 /// and must compute and include all ancestors of the requested entities.
5457 /// Loading more entities than requested is allowed.
5458 pub trait EntityLoader {
5459 /// Load all entities for the given set of entity UIDs.
5460 /// Returns a map from [`EntityUid`] to [`Option<Entity>`], where `None` indicates
5461 /// the entity does not exist.
5462 fn load_entities(
5463 &mut self,
5464 uids: &HashSet<EntityUid>,
5465 ) -> HashMap<EntityUid, Option<Entity>>;
5466 }
5467
5468 /// Wrapper struct used to convert an [`EntityLoader`] to an `EntityLoaderInternal`
5469 struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
5470
5471 impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
5472 fn load_entities(
5473 &mut self,
5474 uids: &HashSet<ast::EntityUID>,
5475 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
5476 let ids = uids
5477 .iter()
5478 .map(|id| EntityUid::ref_cast(id).clone())
5479 .collect();
5480 self.0
5481 .load_entities(&ids)
5482 .into_iter()
5483 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
5484 .collect()
5485 }
5486 }
5487
5488 /// Simple entity loader implementation that loads from a pre-existing Entities store
5489 #[derive(Debug)]
5490
5491 pub struct TestEntityLoader<'a> {
5492 entities: &'a Entities,
5493 }
5494
5495 impl<'a> TestEntityLoader<'a> {
5496 /// Create a new [`TestEntityLoader`] from an existing Entities store
5497 pub fn new(entities: &'a Entities) -> Self {
5498 Self { entities }
5499 }
5500 }
5501
5502 impl EntityLoader for TestEntityLoader<'_> {
5503 fn load_entities(
5504 &mut self,
5505 uids: &HashSet<EntityUid>,
5506 ) -> HashMap<EntityUid, Option<Entity>> {
5507 uids.iter()
5508 .map(|uid| {
5509 let entity = self.entities.get(uid).cloned();
5510 (uid.clone(), entity)
5511 })
5512 .collect()
5513 }
5514 }
5515
5516 impl PolicySet {
5517 /// Perform type-aware partial evaluation on this [`PolicySet`]
5518 /// If successful, the result is a [`PolicySet`] containing residual
5519 /// policies ready for re-authorization
5520 pub fn tpe<'a>(
5521 &self,
5522 request: &'a PartialRequest,
5523 entities: &'a PartialEntities,
5524 schema: &'a Schema,
5525 ) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
5526 use cedar_policy_core::tpe::is_authorized;
5527 let ps = &self.ast;
5528 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
5529 Ok(TpeResponse(res))
5530 }
5531
5532 /// Like [`Authorizer::is_authorized`] but uses an [`EntityLoader`] to load
5533 /// entities on demand.
5534 ///
5535 /// Calls `loader` at most `max_iters` times, returning
5536 /// early if an authorization result is reached.
5537 /// Otherwise, it iterates `max_iters` times and returns
5538 /// a partial result.
5539 ///
5540 pub fn is_authorized_batched(
5541 &self,
5542 query: &Request,
5543 schema: &Schema,
5544 loader: &mut dyn EntityLoader,
5545 max_iters: u32,
5546 ) -> Result<Decision, BatchedEvalError> {
5547 is_authorized_batched(
5548 &query.0,
5549 &self.ast,
5550 &schema.0,
5551 &mut EntityLoaderWrapper(loader),
5552 max_iters,
5553 )
5554 }
5555
5556 /// Perform a permission query on the resource
5557 pub fn query_resource(
5558 &self,
5559 request: &ResourceQueryRequest,
5560 entities: &Entities,
5561 schema: &Schema,
5562 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5563 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5564 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5565 // PANIC SAFETY: policy set construction should succeed because there shouldn't be any policy id conflicts
5566 #[allow(clippy::unwrap_used)]
5567 let policies = &Self::from_policies(
5568 residuals
5569 .0
5570 .residual_policies()
5571 .map(|p| Policy::from_ast(p.clone().into())),
5572 )
5573 .unwrap();
5574 // PANIC SAFETY: request construction should succeed because each entity passes validation
5575 #[allow(clippy::unwrap_used)]
5576 match residuals.decision() {
5577 Some(Decision::Allow) => Ok(entities
5578 .iter()
5579 .filter(|entity| {
5580 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5581 })
5582 .map(super::Entity::uid)
5583 .collect_vec()
5584 .into_iter()),
5585 Some(Decision::Deny) => Ok(vec![].into_iter()),
5586 None => Ok(entities
5587 .iter()
5588 .filter(|entity| {
5589 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5590 })
5591 .filter(|entity| {
5592 let authorizer = Authorizer::new();
5593 authorizer
5594 .is_authorized(
5595 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5596 policies,
5597 entities,
5598 )
5599 .decision
5600 == Decision::Allow
5601 })
5602 .map(super::Entity::uid)
5603 .collect_vec()
5604 .into_iter()),
5605 }
5606 }
5607
5608 /// Perform a permission query on the principal
5609 pub fn query_principal(
5610 &self,
5611 request: &PrincipalQueryRequest,
5612 entities: &Entities,
5613 schema: &Schema,
5614 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5615 let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
5616 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5617 // PANIC SAFETY: policy set construction should succeed because there shouldn't be any policy id conflicts
5618 #[allow(clippy::unwrap_used)]
5619 let policies = &Self::from_policies(
5620 residuals
5621 .0
5622 .residual_policies()
5623 .map(|p| Policy::from_ast(p.clone().into())),
5624 )
5625 .unwrap();
5626 // PANIC SAFETY: request construction should succeed because each entity passes validation
5627 #[allow(clippy::unwrap_used)]
5628 match residuals.decision() {
5629 Some(Decision::Allow) => Ok(entities
5630 .iter()
5631 .filter(|entity| {
5632 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5633 })
5634 .map(super::Entity::uid)
5635 .collect_vec()
5636 .into_iter()),
5637 Some(Decision::Deny) => Ok(vec![].into_iter()),
5638 None => Ok(entities
5639 .iter()
5640 .filter(|entity| {
5641 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5642 })
5643 .filter(|entity| {
5644 let authorizer = Authorizer::new();
5645 authorizer
5646 .is_authorized(
5647 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5648 policies,
5649 entities,
5650 )
5651 .decision
5652 == Decision::Allow
5653 })
5654 .map(super::Entity::uid)
5655 .collect_vec()
5656 .into_iter()),
5657 }
5658 }
5659
5660 /// Given a [`ActionQueryRequest`] (a partial request without a concrete
5661 /// action) enumerate actions in the schema which might be authorized
5662 /// for that request.
5663 ///
5664 /// Each action is returned with a partial authorization decision. If
5665 /// the action is definitely authorized, then it is `Some(Decision::Allow)`.
5666 /// If we did not reach a concrete authorization decision, then it is
5667 /// `None`. Actions which are definitely not authorized (i.e., the
5668 /// decision is `Some(Decision::Deny)`) are not returned by this
5669 /// function. It is also possible that some actions without a concrete
5670 /// authorization decision are never authorized if the residual
5671 /// expressions after partial evaluation are not satisfiable.
5672 ///
5673 /// If the partial request for a particular action is invalid (e.g., the
5674 /// action does not apply to the type of principal and resource), then
5675 /// that action is not included in the result regardless of whether a
5676 /// request with that action would be authorized.
5677 ///
5678 /// ```
5679 /// # use cedar_policy::{PolicySet, Schema, ActionQueryRequest, PartialEntities, PartialEntityUid, Decision, EntityUid, Entities};
5680 /// # use std::str::FromStr;
5681 /// # let policies = PolicySet::from_str(r#"
5682 /// # permit(principal, action == Action::"edit", resource) when { context.should_allow };
5683 /// # permit(principal, action == Action::"view", resource);
5684 /// # "#).unwrap();
5685 /// # let schema = Schema::from_str("
5686 /// # entity User, Photo;
5687 /// # action view, edit appliesTo {
5688 /// # principal: User,
5689 /// # resource: Photo,
5690 /// # context: { should_allow: Bool, }
5691 /// # };
5692 /// # ").unwrap();
5693 /// # let entities = PartialEntities::empty();
5694 ///
5695 /// // Construct a request for a concrete principal and resource, but leaving the context unknown so
5696 /// // that we can see all actions that might be authorized for some context.
5697 /// let request = ActionQueryRequest::new(
5698 /// PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
5699 /// PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
5700 /// None,
5701 /// schema,
5702 /// ).unwrap();
5703 ///
5704 /// // All actions which might be allowed for this principal and resource.
5705 /// // The exact authorization result may depend on currently unknown
5706 /// // context and entity data.
5707 /// let possibly_allowed_actions: Vec<&EntityUid> =
5708 /// policies.query_action(&request, &entities)
5709 /// .unwrap()
5710 /// .map(|(a, _)| a)
5711 /// .collect();
5712 /// # let mut possibly_allowed_actions = possibly_allowed_actions;
5713 /// # possibly_allowed_actions.sort();
5714 /// # assert_eq!(&possibly_allowed_actions, &[&r#"Action::"edit""#.parse().unwrap(), &r#"Action::"view""#.parse().unwrap()]);
5715 ///
5716 /// // These actions are definitely allowed for this principal and resource.
5717 /// // These will be allowed for _any_ context.
5718 /// let allowed_actions: Vec<&EntityUid> =
5719 /// policies.query_action(&request, &entities).unwrap()
5720 /// .filter(|(_, resp)| resp == &Some(Decision::Allow))
5721 /// .map(|(a, _)| a)
5722 /// .collect();
5723 /// # assert_eq!(&allowed_actions, &[&r#"Action::"view""#.parse().unwrap()]);
5724 /// ```
5725 pub fn query_action<'a>(
5726 &self,
5727 request: &'a ActionQueryRequest,
5728 entities: &PartialEntities,
5729 ) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError>
5730 {
5731 let mut authorized_actions = Vec::new();
5732 // We only consider actions that apply to the type of the requested
5733 // principal and resource. Any requests for different actions would
5734 // be invalid, so they should never be authorized. Not however that
5735 // an authorization request for _could_ return `Allow` if the caller
5736 // ignores the request validation error.
5737 for action in request
5738 .schema
5739 .0
5740 .actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
5741 {
5742 // If we fail to construct a partial request, then the partial context is not valid for
5743 // the context type declared for this action. This action should never be authorized,
5744 // but with the same caveats about invalid requests.
5745 if let Ok(partial_request) = request.partial_request(action.clone().into()) {
5746 let decision = self
5747 .tpe(&partial_request, entities, &request.schema)?
5748 .decision();
5749 if decision != Some(Decision::Deny) {
5750 authorized_actions.push((RefCast::ref_cast(action), decision));
5751 }
5752 }
5753 }
5754 Ok(authorized_actions.into_iter())
5755 }
5756 }
5757}
5758
5759// These are the same tests in validator, just ensuring all the plumbing is done correctly
5760#[cfg(test)]
5761mod test_access {
5762 use cedar_policy_core::ast;
5763
5764 use super::*;
5765
5766 fn schema() -> Schema {
5767 let src = r#"
5768 type Task = {
5769 "id": Long,
5770 "name": String,
5771 "state": String,
5772};
5773
5774type T = String;
5775
5776type Tasks = Set<Task>;
5777entity List in [Application] = {
5778 "editors": Team,
5779 "name": String,
5780 "owner": User,
5781 "readers": Team,
5782 "tasks": Tasks,
5783};
5784entity Application;
5785entity User in [Team, Application] = {
5786 "joblevel": Long,
5787 "location": String,
5788};
5789
5790entity CoolList;
5791
5792entity Team in [Team, Application];
5793
5794action Read, Write, Create;
5795
5796action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
5797 principal: [User],
5798 resource : [List]
5799};
5800
5801action GetList in Read appliesTo {
5802 principal : [User],
5803 resource : [List, CoolList]
5804};
5805
5806action GetLists in Read appliesTo {
5807 principal : [User],
5808 resource : [Application]
5809};
5810
5811action CreateList in Create appliesTo {
5812 principal : [User],
5813 resource : [Application]
5814};
5815
5816 "#;
5817
5818 src.parse().unwrap()
5819 }
5820
5821 #[test]
5822 fn principals() {
5823 let schema = schema();
5824 let principals = schema.principals().collect::<HashSet<_>>();
5825 assert_eq!(principals.len(), 1);
5826 let user: EntityTypeName = "User".parse().unwrap();
5827 assert!(principals.contains(&user));
5828 let principals = schema.principals().collect::<Vec<_>>();
5829 assert!(principals.len() > 1);
5830 assert!(principals.iter().all(|ety| **ety == user));
5831 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
5832
5833 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
5834 let et = schema.0.get_entity_type(&et).unwrap();
5835 assert!(et.loc.is_some());
5836 }
5837
5838 #[cfg(feature = "extended-schema")]
5839 #[test]
5840 fn common_types_extended() {
5841 use cool_asserts::assert_matches;
5842
5843 use cedar_policy_core::validator::{
5844 types::{EntityRecordKind, Type},
5845 LocatedCommonType,
5846 };
5847
5848 let schema = schema();
5849 assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
5850 let task_type = LocatedCommonType {
5851 name: "Task".into(),
5852 name_loc: None,
5853 type_loc: None,
5854 };
5855 assert!(schema.0.common_types().contains(&task_type));
5856
5857 let tasks_type = LocatedCommonType {
5858 name: "Tasks".into(),
5859 name_loc: None,
5860 type_loc: None,
5861 };
5862 assert!(schema.0.common_types().contains(&tasks_type));
5863 assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
5864 assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
5865
5866 let tasks_type = LocatedCommonType {
5867 name: "T".into(),
5868 name_loc: None,
5869 type_loc: None,
5870 };
5871 assert!(schema.0.common_types().contains(&tasks_type));
5872
5873 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
5874 let et = schema.0.get_entity_type(&et).unwrap();
5875 let attrs = et.attributes();
5876
5877 // Assert that attributes that are resolved from common types still get source locations
5878 let t = attrs.get_attr("tasks").unwrap();
5879 assert!(t.loc.is_some());
5880 assert_matches!(&t.attr_type, cedar_policy_core::validator::types::Type::Set { ref element_type } => {
5881 let el = *element_type.clone().unwrap();
5882 assert_matches!(el, Type::EntityOrRecord(EntityRecordKind::Record { attrs, .. }) => {
5883 assert!(attrs.get_attr("name").unwrap().loc.is_some());
5884 assert!(attrs.get_attr("id").unwrap().loc.is_some());
5885 assert!(attrs.get_attr("state").unwrap().loc.is_some());
5886 });
5887 });
5888 }
5889
5890 #[cfg(feature = "extended-schema")]
5891 #[test]
5892 fn namespace_extended() {
5893 let schema = schema();
5894 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
5895 let default_namespace = schema.0.namespaces().last().unwrap();
5896 assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
5897 assert!(default_namespace.name_loc.is_none());
5898 assert!(default_namespace.def_loc.is_none());
5899 }
5900
5901 #[test]
5902 fn empty_schema_principals_and_resources() {
5903 let empty: Schema = "".parse().unwrap();
5904 assert!(empty.principals().next().is_none());
5905 assert!(empty.resources().next().is_none());
5906 }
5907
5908 #[test]
5909 fn resources() {
5910 let schema = schema();
5911 let resources = schema.resources().cloned().collect::<HashSet<_>>();
5912 let expected: HashSet<EntityTypeName> = HashSet::from([
5913 "List".parse().unwrap(),
5914 "Application".parse().unwrap(),
5915 "CoolList".parse().unwrap(),
5916 ]);
5917 assert_eq!(resources, expected);
5918 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
5919 }
5920
5921 #[test]
5922 fn principals_for_action() {
5923 let schema = schema();
5924 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
5925 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
5926 let got = schema
5927 .principals_for_action(&delete_list)
5928 .unwrap()
5929 .cloned()
5930 .collect::<Vec<_>>();
5931 assert_eq!(got, vec!["User".parse().unwrap()]);
5932 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5933 assert!(schema.principals_for_action(&delete_user).is_none());
5934 }
5935
5936 #[test]
5937 fn resources_for_action() {
5938 let schema = schema();
5939 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
5940 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
5941 let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
5942 let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
5943 let got = schema
5944 .resources_for_action(&delete_list)
5945 .unwrap()
5946 .cloned()
5947 .collect::<Vec<_>>();
5948 assert_eq!(got, vec!["List".parse().unwrap()]);
5949 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5950 let got = schema
5951 .resources_for_action(&create_list)
5952 .unwrap()
5953 .cloned()
5954 .collect::<Vec<_>>();
5955 assert_eq!(got, vec!["Application".parse().unwrap()]);
5956 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5957 let got = schema
5958 .resources_for_action(&get_list)
5959 .unwrap()
5960 .cloned()
5961 .collect::<HashSet<_>>();
5962 assert_eq!(
5963 got,
5964 HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
5965 );
5966 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5967 assert!(schema.principals_for_action(&delete_user).is_none());
5968 }
5969
5970 #[test]
5971 fn principal_parents() {
5972 let schema = schema();
5973 let user: EntityTypeName = "User".parse().unwrap();
5974 let parents = schema
5975 .ancestors(&user)
5976 .unwrap()
5977 .cloned()
5978 .collect::<HashSet<_>>();
5979 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5980 let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
5981 assert_eq!(parents, expected);
5982 let parents = schema
5983 .ancestors(&"List".parse().unwrap())
5984 .unwrap()
5985 .cloned()
5986 .collect::<HashSet<_>>();
5987 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5988 let expected = HashSet::from(["Application".parse().unwrap()]);
5989 assert_eq!(parents, expected);
5990 assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
5991 let parents = schema
5992 .ancestors(&"CoolList".parse().unwrap())
5993 .unwrap()
5994 .cloned()
5995 .collect::<HashSet<_>>();
5996 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5997 let expected = HashSet::from([]);
5998 assert_eq!(parents, expected);
5999 }
6000
6001 #[test]
6002 fn action_groups() {
6003 let schema = schema();
6004 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6005 let expected = ["Read", "Write", "Create"]
6006 .into_iter()
6007 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6008 .collect::<HashSet<EntityUid>>();
6009 #[cfg(feature = "extended-schema")]
6010 assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
6011 assert_eq!(groups, expected);
6012 }
6013
6014 #[test]
6015 fn actions() {
6016 let schema = schema();
6017 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6018 let expected = [
6019 "Read",
6020 "Write",
6021 "Create",
6022 "DeleteList",
6023 "EditShare",
6024 "UpdateList",
6025 "CreateTask",
6026 "UpdateTask",
6027 "DeleteTask",
6028 "GetList",
6029 "GetLists",
6030 "CreateList",
6031 ]
6032 .into_iter()
6033 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6034 .collect::<HashSet<EntityUid>>();
6035 assert_eq!(actions, expected);
6036 #[cfg(feature = "extended-schema")]
6037 assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
6038 }
6039
6040 #[test]
6041 fn actions_for_principal_and_resource() {
6042 let schema = schema();
6043 let pty: EntityTypeName = "User".parse().unwrap();
6044 let rty: EntityTypeName = "Application".parse().unwrap();
6045 let actions = schema
6046 .actions_for_principal_and_resource(&pty, &rty)
6047 .cloned()
6048 .collect::<HashSet<EntityUid>>();
6049 let expected = ["GetLists", "CreateList"]
6050 .into_iter()
6051 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
6052 .collect::<HashSet<EntityUid>>();
6053 assert_eq!(actions, expected);
6054 }
6055
6056 #[test]
6057 fn entities() {
6058 let schema = schema();
6059 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6060 let expected = ["List", "Application", "User", "CoolList", "Team"]
6061 .into_iter()
6062 .map(|ty| ty.parse().unwrap())
6063 .collect::<HashSet<EntityTypeName>>();
6064 assert_eq!(entities, expected);
6065 }
6066}
6067
6068#[cfg(test)]
6069mod test_access_namespace {
6070 use super::*;
6071
6072 fn schema() -> Schema {
6073 let src = r#"
6074 namespace Foo {
6075 type Task = {
6076 "id": Long,
6077 "name": String,
6078 "state": String,
6079};
6080
6081type Tasks = Set<Task>;
6082entity List in [Application] = {
6083 "editors": Team,
6084 "name": String,
6085 "owner": User,
6086 "readers": Team,
6087 "tasks": Tasks,
6088};
6089entity Application;
6090entity User in [Team, Application] = {
6091 "joblevel": Long,
6092 "location": String,
6093};
6094
6095entity CoolList;
6096
6097entity Team in [Team, Application];
6098
6099action Read, Write, Create;
6100
6101action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
6102 principal: [User],
6103 resource : [List]
6104};
6105
6106action GetList in Read appliesTo {
6107 principal : [User],
6108 resource : [List, CoolList]
6109};
6110
6111action GetLists in Read appliesTo {
6112 principal : [User],
6113 resource : [Application]
6114};
6115
6116action CreateList in Create appliesTo {
6117 principal : [User],
6118 resource : [Application]
6119};
6120 }
6121
6122 "#;
6123
6124 src.parse().unwrap()
6125 }
6126
6127 #[test]
6128 fn principals() {
6129 let schema = schema();
6130 let principals = schema.principals().collect::<HashSet<_>>();
6131 assert_eq!(principals.len(), 1);
6132 let user: EntityTypeName = "Foo::User".parse().unwrap();
6133 assert!(principals.contains(&user));
6134 let principals = schema.principals().collect::<Vec<_>>();
6135 assert!(principals.len() > 1);
6136 assert!(principals.iter().all(|ety| **ety == user));
6137 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
6138 }
6139
6140 #[test]
6141 fn empty_schema_principals_and_resources() {
6142 let empty: Schema = "".parse().unwrap();
6143 assert!(empty.principals().next().is_none());
6144 assert!(empty.resources().next().is_none());
6145 }
6146
6147 #[test]
6148 fn resources() {
6149 let schema = schema();
6150 let resources = schema.resources().cloned().collect::<HashSet<_>>();
6151 let expected: HashSet<EntityTypeName> = HashSet::from([
6152 "Foo::List".parse().unwrap(),
6153 "Foo::Application".parse().unwrap(),
6154 "Foo::CoolList".parse().unwrap(),
6155 ]);
6156 assert_eq!(resources, expected);
6157 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
6158 }
6159
6160 #[test]
6161 fn principals_for_action() {
6162 let schema = schema();
6163 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6164 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6165 let got = schema
6166 .principals_for_action(&delete_list)
6167 .unwrap()
6168 .cloned()
6169 .collect::<Vec<_>>();
6170 assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
6171 assert!(schema.principals_for_action(&delete_user).is_none());
6172 }
6173
6174 #[test]
6175 fn resources_for_action() {
6176 let schema = schema();
6177 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
6178 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
6179 let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
6180 let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
6181 let got = schema
6182 .resources_for_action(&delete_list)
6183 .unwrap()
6184 .cloned()
6185 .collect::<Vec<_>>();
6186 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6187
6188 assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
6189 let got = schema
6190 .resources_for_action(&create_list)
6191 .unwrap()
6192 .cloned()
6193 .collect::<Vec<_>>();
6194 assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
6195 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
6196
6197 let got = schema
6198 .resources_for_action(&get_list)
6199 .unwrap()
6200 .cloned()
6201 .collect::<HashSet<_>>();
6202 assert_eq!(
6203 got,
6204 HashSet::from([
6205 "Foo::List".parse().unwrap(),
6206 "Foo::CoolList".parse().unwrap()
6207 ])
6208 );
6209 assert!(schema.principals_for_action(&delete_user).is_none());
6210 }
6211
6212 #[test]
6213 fn principal_parents() {
6214 let schema = schema();
6215 let user: EntityTypeName = "Foo::User".parse().unwrap();
6216 let parents = schema
6217 .ancestors(&user)
6218 .unwrap()
6219 .cloned()
6220 .collect::<HashSet<_>>();
6221 let expected = HashSet::from([
6222 "Foo::Team".parse().unwrap(),
6223 "Foo::Application".parse().unwrap(),
6224 ]);
6225 assert_eq!(parents, expected);
6226 let parents = schema
6227 .ancestors(&"Foo::List".parse().unwrap())
6228 .unwrap()
6229 .cloned()
6230 .collect::<HashSet<_>>();
6231 let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
6232 assert_eq!(parents, expected);
6233 assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
6234 let parents = schema
6235 .ancestors(&"Foo::CoolList".parse().unwrap())
6236 .unwrap()
6237 .cloned()
6238 .collect::<HashSet<_>>();
6239 let expected = HashSet::from([]);
6240 assert_eq!(parents, expected);
6241 }
6242
6243 #[test]
6244 fn action_groups() {
6245 let schema = schema();
6246 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
6247 let expected = ["Read", "Write", "Create"]
6248 .into_iter()
6249 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6250 .collect::<HashSet<EntityUid>>();
6251 assert_eq!(groups, expected);
6252 }
6253
6254 #[test]
6255 fn actions() {
6256 let schema = schema();
6257 let actions = schema.actions().cloned().collect::<HashSet<_>>();
6258 let expected = [
6259 "Read",
6260 "Write",
6261 "Create",
6262 "DeleteList",
6263 "EditShare",
6264 "UpdateList",
6265 "CreateTask",
6266 "UpdateTask",
6267 "DeleteTask",
6268 "GetList",
6269 "GetLists",
6270 "CreateList",
6271 ]
6272 .into_iter()
6273 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
6274 .collect::<HashSet<EntityUid>>();
6275 assert_eq!(actions, expected);
6276 }
6277
6278 #[test]
6279 fn entities() {
6280 let schema = schema();
6281 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
6282 let expected = [
6283 "Foo::List",
6284 "Foo::Application",
6285 "Foo::User",
6286 "Foo::CoolList",
6287 "Foo::Team",
6288 ]
6289 .into_iter()
6290 .map(|ty| ty.parse().unwrap())
6291 .collect::<HashSet<EntityTypeName>>();
6292 assert_eq!(entities, expected);
6293 }
6294
6295 #[test]
6296 fn test_request_context() {
6297 // Create a context with some test data
6298 let context =
6299 Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
6300
6301 // Create entity UIDs for the request
6302 let principal: EntityUid = "User::\"alice\"".parse().unwrap();
6303 let action: EntityUid = "Action::\"view\"".parse().unwrap();
6304 let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
6305
6306 // Create the request
6307 let request = Request::new(
6308 principal, action, resource, context, None, // no schema validation for this test
6309 )
6310 .unwrap();
6311
6312 // Test context() method
6313 let retrieved_context = request.context().expect("Context should be present");
6314
6315 // Test get() method on the retrieved context
6316 assert!(retrieved_context.get("testKey").is_some());
6317 assert!(retrieved_context.get("numKey").is_some());
6318 assert!(retrieved_context.get("nonexistent").is_none());
6319 }
6320
6321 #[cfg(feature = "extended-schema")]
6322 #[test]
6323 fn namespace_extended() {
6324 let schema = schema();
6325 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
6326 let default_namespace = schema
6327 .0
6328 .namespaces()
6329 .filter(|n| n.name == *"__cedar")
6330 .last()
6331 .unwrap();
6332 assert!(default_namespace.name_loc.is_none());
6333 assert!(default_namespace.def_loc.is_none());
6334
6335 let default_namespace = schema
6336 .0
6337 .namespaces()
6338 .filter(|n| n.name == *"Foo")
6339 .last()
6340 .unwrap();
6341 assert!(default_namespace.name_loc.is_some());
6342 assert!(default_namespace.def_loc.is_some());
6343 }
6344}
6345
6346#[cfg(test)]
6347mod test_lossless_empty {
6348 use super::{LosslessPolicy, Policy, PolicyId, Template};
6349
6350 #[test]
6351 fn test_lossless_empty_policy() {
6352 const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
6353 let policy0 = Policy::parse(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
6354 .expect("Failed to parse");
6355 let lossy_policy0 = Policy {
6356 ast: policy0.ast.clone(),
6357 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6358 };
6359 // The `to_cedar` representation becomes lossy since we didn't provide text
6360 assert_eq!(
6361 lossy_policy0.to_cedar(),
6362 Some(String::from(
6363 "permit(\n principal,\n action,\n resource\n);"
6364 ))
6365 );
6366 // The EST representation is obtained from the AST
6367 let lossy_policy0_est = lossy_policy0
6368 .lossless
6369 .est(|| policy0.ast.clone().into())
6370 .unwrap();
6371 assert_eq!(lossy_policy0_est, policy0.ast.into());
6372 }
6373
6374 #[test]
6375 fn test_lossless_empty_template() {
6376 const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
6377 let template0 = Template::parse(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
6378 .expect("Failed to parse");
6379 let lossy_template0 = Template {
6380 ast: template0.ast.clone(),
6381 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6382 };
6383 // The `to_cedar` representation becomes lossy since we didn't provide text
6384 assert_eq!(
6385 lossy_template0.to_cedar(),
6386 String::from("permit(\n principal == ?principal,\n action,\n resource\n);")
6387 );
6388 // The EST representation is obtained from the AST
6389 let lossy_template0_est = lossy_template0
6390 .lossless
6391 .est(|| template0.ast.clone().into())
6392 .unwrap();
6393 assert_eq!(lossy_template0_est, template0.ast.into());
6394 }
6395}
6396
6397/// Given a schema and policy set, compute an entity manifest.
6398///
6399/// The policies must validate against the schema in strict mode,
6400/// otherwise an error is returned.
6401/// The manifest describes the data required to answer requests
6402/// for each action.
6403#[doc = include_str!("../experimental_warning.md")]
6404#[cfg(feature = "entity-manifest")]
6405pub fn compute_entity_manifest(
6406 validator: &Validator,
6407 pset: &PolicySet,
6408) -> Result<EntityManifest, EntityManifestError> {
6409 entity_manifest::compute_entity_manifest(&validator.0, &pset.ast)
6410 .map_err(std::convert::Into::into)
6411}