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