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
2082/// Contains the result of policy validation.
2083///
2084/// The result includes the list of issues found by validation and whether validation succeeds or fails.
2085/// Validation succeeds if there are no fatal errors. There may still be
2086/// non-fatal warnings present when validation passes.
2087#[derive(Debug, Clone)]
2088pub struct ValidationResult {
2089 validation_errors: Vec<ValidationError>,
2090 validation_warnings: Vec<ValidationWarning>,
2091}
2092
2093impl ValidationResult {
2094 /// True when validation passes. There are no errors, but there may be
2095 /// non-fatal warnings. Use [`ValidationResult::validation_passed_without_warnings`]
2096 /// to check that there are also no warnings.
2097 pub fn validation_passed(&self) -> bool {
2098 self.validation_errors.is_empty()
2099 }
2100
2101 /// True when validation passes (i.e., there are no errors) and there are
2102 /// additionally no non-fatal warnings.
2103 pub fn validation_passed_without_warnings(&self) -> bool {
2104 self.validation_errors.is_empty() && self.validation_warnings.is_empty()
2105 }
2106
2107 /// Get an iterator over the errors found by the validator.
2108 pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
2109 self.validation_errors.iter()
2110 }
2111
2112 /// Get an iterator over the warnings found by the validator.
2113 pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
2114 self.validation_warnings.iter()
2115 }
2116
2117 fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
2118 self.validation_errors
2119 .first()
2120 .map(|e| e as &dyn Diagnostic)
2121 .or_else(|| {
2122 self.validation_warnings
2123 .first()
2124 .map(|w| w as &dyn Diagnostic)
2125 })
2126 }
2127
2128 pub(crate) fn into_errors_and_warnings(
2129 self,
2130 ) -> (
2131 impl Iterator<Item = ValidationError>,
2132 impl Iterator<Item = ValidationWarning>,
2133 ) {
2134 (
2135 self.validation_errors.into_iter(),
2136 self.validation_warnings.into_iter(),
2137 )
2138 }
2139}
2140
2141#[doc(hidden)]
2142impl From<cedar_policy_core::validator::ValidationResult> for ValidationResult {
2143 fn from(r: cedar_policy_core::validator::ValidationResult) -> Self {
2144 let (errors, warnings) = r.into_errors_and_warnings();
2145 Self {
2146 validation_errors: errors.map(ValidationError::from).collect(),
2147 validation_warnings: warnings.map(ValidationWarning::from).collect(),
2148 }
2149 }
2150}
2151
2152impl std::fmt::Display for ValidationResult {
2153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2154 match self.first_error_or_warning() {
2155 Some(diagnostic) => write!(f, "{diagnostic}"),
2156 None => write!(f, "no errors or warnings"),
2157 }
2158 }
2159}
2160
2161impl std::error::Error for ValidationResult {
2162 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
2163 self.first_error_or_warning()
2164 .and_then(std::error::Error::source)
2165 }
2166
2167 #[allow(deprecated)]
2168 fn description(&self) -> &str {
2169 self.first_error_or_warning()
2170 .map_or("no errors or warnings", std::error::Error::description)
2171 }
2172
2173 #[allow(deprecated)]
2174 fn cause(&self) -> Option<&dyn std::error::Error> {
2175 self.first_error_or_warning()
2176 .and_then(std::error::Error::cause)
2177 }
2178}
2179
2180// Except for `.related()`, and `.severity` everything is forwarded to the first
2181// error, or to the first warning if there are no errors. This is done for the
2182// same reason as policy parse errors.
2183impl Diagnostic for ValidationResult {
2184 fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
2185 let mut related = self
2186 .validation_errors
2187 .iter()
2188 .map(|err| err as &dyn Diagnostic)
2189 .chain(
2190 self.validation_warnings
2191 .iter()
2192 .map(|warn| warn as &dyn Diagnostic),
2193 );
2194 related.next().map(move |first| match first.related() {
2195 Some(first_related) => Box::new(first_related.chain(related)),
2196 None => Box::new(related) as Box<dyn Iterator<Item = _>>,
2197 })
2198 }
2199
2200 fn severity(&self) -> Option<miette::Severity> {
2201 self.first_error_or_warning()
2202 .map_or(Some(miette::Severity::Advice), Diagnostic::severity)
2203 }
2204
2205 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
2206 self.first_error_or_warning().and_then(Diagnostic::labels)
2207 }
2208
2209 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
2210 self.first_error_or_warning()
2211 .and_then(Diagnostic::source_code)
2212 }
2213
2214 fn code(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2215 self.first_error_or_warning().and_then(Diagnostic::code)
2216 }
2217
2218 fn url(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2219 self.first_error_or_warning().and_then(Diagnostic::url)
2220 }
2221
2222 fn help(&self) -> Option<Box<dyn std::fmt::Display + '_>> {
2223 self.first_error_or_warning().and_then(Diagnostic::help)
2224 }
2225
2226 fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
2227 self.first_error_or_warning()
2228 .and_then(Diagnostic::diagnostic_source)
2229 }
2230}
2231
2232/// Scan a set of policies for potentially confusing/obfuscating text.
2233///
2234/// These checks are also provided through [`Validator::validate`] which provides more
2235/// comprehensive error detection, but this function can be used to check for
2236/// confusable strings without defining a schema.
2237pub fn confusable_string_checker<'a>(
2238 templates: impl Iterator<Item = &'a Template> + 'a,
2239) -> impl Iterator<Item = ValidationWarning> + 'a {
2240 cedar_policy_core::validator::confusable_string_checks(templates.map(|t| &t.ast))
2241 .map(std::convert::Into::into)
2242}
2243
2244/// Represents a namespace.
2245///
2246/// An `EntityNamespace` can can be constructed using
2247/// [`EntityNamespace::from_str`] or by calling `parse()` on a string.
2248/// _This can fail_, so it is important to properly handle an `Err` result.
2249///
2250/// ```
2251/// # use cedar_policy::EntityNamespace;
2252/// let id : Result<EntityNamespace, _> = "My::Name::Space".parse();
2253/// # assert_eq!(id.unwrap().to_string(), "My::Name::Space".to_string());
2254/// ```
2255#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
2256pub struct EntityNamespace(pub(crate) ast::Name);
2257
2258#[doc(hidden)] // because this converts to a private/internal type
2259impl AsRef<ast::Name> for EntityNamespace {
2260 fn as_ref(&self) -> &ast::Name {
2261 &self.0
2262 }
2263}
2264
2265/// This `FromStr` implementation requires the _normalized_ representation of the
2266/// namespace. See <https://github.com/cedar-policy/rfcs/pull/9/>.
2267impl FromStr for EntityNamespace {
2268 type Err = ParseErrors;
2269
2270 fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
2271 ast::Name::from_normalized_str(namespace_str)
2272 .map(EntityNamespace)
2273 .map_err(Into::into)
2274 }
2275}
2276
2277impl std::fmt::Display for EntityNamespace {
2278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2279 write!(f, "{}", self.0)
2280 }
2281}
2282
2283#[derive(Debug, Clone, Default)]
2284/// A struct representing a `PolicySet` as a series of strings for ser/de.
2285/// A `PolicySet` that contains template-linked policies cannot be
2286/// represented as this struct.
2287pub(crate) struct StringifiedPolicySet {
2288 /// The static policies in the set
2289 pub policies: Vec<String>,
2290 /// The policy templates in the set
2291 pub policy_templates: Vec<String>,
2292}
2293
2294/// Represents a set of `Policy`s
2295#[derive(Debug, Clone, Default)]
2296pub struct PolicySet {
2297 /// AST representation. Technically partially redundant with the other fields.
2298 /// Internally, we ensure that the duplicated information remains consistent.
2299 pub(crate) ast: ast::PolicySet,
2300 /// Policies in the set (this includes both static policies and template linked-policies)
2301 policies: LinkedHashMap<PolicyId, Policy>,
2302 /// Templates in the set
2303 templates: LinkedHashMap<PolicyId, Template>,
2304}
2305
2306impl PartialEq for PolicySet {
2307 fn eq(&self, other: &Self) -> bool {
2308 // eq is based on just the `ast`
2309 self.ast.eq(&other.ast)
2310 }
2311}
2312impl Eq for PolicySet {}
2313
2314#[doc(hidden)] // because this converts to a private/internal type
2315impl AsRef<ast::PolicySet> for PolicySet {
2316 fn as_ref(&self) -> &ast::PolicySet {
2317 &self.ast
2318 }
2319}
2320
2321#[doc(hidden)]
2322impl TryFrom<ast::PolicySet> for PolicySet {
2323 type Error = PolicySetError;
2324 fn try_from(pset: ast::PolicySet) -> Result<Self, Self::Error> {
2325 Self::from_ast(pset)
2326 }
2327}
2328
2329impl FromStr for PolicySet {
2330 type Err = ParseErrors;
2331
2332 /// Create a policy set from multiple statements.
2333 ///
2334 /// Policy ids will default to "policy*" with numbers from 0.
2335 /// If you load more policies, do not use the default id, or there will be conflicts.
2336 ///
2337 /// See [`Policy`] for more.
2338 fn from_str(policies: &str) -> Result<Self, Self::Err> {
2339 let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
2340 // 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`.
2341 #[allow(clippy::expect_used)]
2342 let policies = pset.policies().map(|p|
2343 (
2344 PolicyId::new(p.id().clone()),
2345 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() }
2346 )
2347 ).collect();
2348 // PANIC SAFETY: By the same invariant, every `PolicyId` in `pset.templates()` also occurs as a key in `text`.
2349 #[allow(clippy::expect_used)]
2350 let templates = pset.templates().map(|t|
2351 (
2352 PolicyId::new(t.id().clone()),
2353 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() }
2354 )
2355 ).collect();
2356 Ok(Self {
2357 ast: pset,
2358 policies,
2359 templates,
2360 })
2361 }
2362}
2363
2364impl PolicySet {
2365 /// Build the policy set AST from the EST
2366 fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
2367 let ast: ast::PolicySet = est.clone().try_into()?;
2368 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.policies()` occurs in `est`
2369 #[allow(clippy::expect_used)]
2370 let policies = ast
2371 .policies()
2372 .map(|p| {
2373 (
2374 PolicyId::new(p.id().clone()),
2375 Policy {
2376 lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
2377 "internal invariant violation: policy id exists in asts but not ests",
2378 )),
2379 ast: p.clone(),
2380 },
2381 )
2382 })
2383 .collect();
2384 // PANIC SAFETY: Since conversion from EST to AST succeeded, every `PolicyId` in `ast.templates()` occurs in `est`
2385 #[allow(clippy::expect_used)]
2386 let templates = ast
2387 .templates()
2388 .map(|t| {
2389 (
2390 PolicyId::new(t.id().clone()),
2391 Template {
2392 lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
2393 "internal invariant violation: template id exists in asts but not ests",
2394 )),
2395 ast: t.clone(),
2396 },
2397 )
2398 })
2399 .collect();
2400 Ok(Self {
2401 ast,
2402 policies,
2403 templates,
2404 })
2405 }
2406
2407 /// Build the [`PolicySet`] from just the AST information
2408 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
2409 pub(crate) fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
2410 Self::from_policies(ast.into_policies().map(Policy::from_ast))
2411 }
2412
2413 /// Deserialize the [`PolicySet`] from a JSON string
2414 pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
2415 let est: est::PolicySet = serde_json::from_str(src.as_ref())
2416 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2417 Self::from_est(&est)
2418 }
2419
2420 /// Deserialize the [`PolicySet`] from a JSON value
2421 pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
2422 let est: est::PolicySet = serde_json::from_value(src)
2423 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2424 Self::from_est(&est)
2425 }
2426
2427 /// Deserialize the [`PolicySet`] from a JSON reader
2428 pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
2429 let est: est::PolicySet = serde_json::from_reader(r)
2430 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2431 Self::from_est(&est)
2432 }
2433
2434 /// Serialize the [`PolicySet`] as a JSON value
2435 pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
2436 let est = self.est()?;
2437 let value = serde_json::to_value(est)
2438 .map_err(|e| policy_set_errors::JsonPolicySetError { inner: e })?;
2439 Ok(value)
2440 }
2441
2442 /// Get the EST representation of the [`PolicySet`]
2443 fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
2444 let (static_policies, template_links): (Vec<_>, Vec<_>) =
2445 fold_partition(self.policies, is_static_or_link)?;
2446 let static_policies = static_policies.into_iter().collect::<LinkedHashMap<_, _>>();
2447 let templates = self
2448 .templates
2449 .into_iter()
2450 .map(|(id, template)| {
2451 template
2452 .lossless
2453 .est(|| template.ast.clone().into())
2454 .map(|est| (id.into(), est))
2455 })
2456 .collect::<Result<LinkedHashMap<_, _>, _>>()?;
2457 let est = est::PolicySet {
2458 templates,
2459 static_policies,
2460 template_links,
2461 };
2462
2463 Ok(est)
2464 }
2465
2466 /// Get the human-readable Cedar syntax representation of this policy set.
2467 /// This function is primarily intended for rendering JSON policies in the
2468 /// human-readable syntax, but it will also return the original policy text
2469 /// (though possibly re-ordering policies within the policy set) when the
2470 /// policy-set contains policies parsed from the human-readable syntax.
2471 ///
2472 /// This will return `None` if there are any linked policies in the policy
2473 /// set because they cannot be directly rendered in Cedar syntax. It also
2474 /// cannot record policy ids because these cannot be specified in the Cedar
2475 /// syntax. The policies may be reordered, so parsing the resulting string
2476 /// with [`PolicySet::from_str`] is likely to yield different policy id
2477 /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2478 /// only using this function to obtain a representation to display to human
2479 /// users.
2480 ///
2481 /// This function does not format the policy according to any particular
2482 /// rules. Policy formatting can be done through the Cedar policy CLI or
2483 /// the `cedar-policy-formatter` crate.
2484 pub fn to_cedar(&self) -> Option<String> {
2485 match self.stringify() {
2486 Some(StringifiedPolicySet {
2487 policies,
2488 policy_templates,
2489 }) => {
2490 let policies_as_vec = policies
2491 .into_iter()
2492 .chain(policy_templates)
2493 .collect::<Vec<_>>();
2494 Some(policies_as_vec.join("\n\n"))
2495 }
2496 None => None,
2497 }
2498 }
2499
2500 /// Get the human-readable Cedar syntax representation of this policy set,
2501 /// as a vec of strings. This function is useful to break up a large cedar
2502 /// file containing many policies into individual policies.
2503 ///
2504 /// This will return `None` if there are any linked policies in the policy
2505 /// set because they cannot be directly rendered in Cedar syntax. It also
2506 /// cannot record policy ids because these cannot be specified in the Cedar
2507 /// syntax. The policies may be reordered, so parsing the resulting string
2508 /// with [`PolicySet::from_str`] is likely to yield different policy id
2509 /// assignments. For these reasons you should prefer serializing as JSON (or protobuf) and
2510 /// only using this function to obtain a compact cedar representation,
2511 /// perhaps for storage purposes.
2512 ///
2513 /// This function does not format the policy according to any particular
2514 /// rules. Policy formatting can be done through the Cedar policy CLI or
2515 /// the `cedar-policy-formatter` crate.
2516 pub(crate) fn stringify(&self) -> Option<StringifiedPolicySet> {
2517 let policies = self
2518 .policies
2519 .values()
2520 // We'd like to print policies in a deterministic order, so we sort
2521 // before printing, hoping that the size of policy sets is fairly
2522 // small.
2523 .sorted_by_key(|p| AsRef::<str>::as_ref(p.id()))
2524 .map(Policy::to_cedar)
2525 .collect::<Option<Vec<_>>>()?;
2526 let policy_templates = self
2527 .templates
2528 .values()
2529 .sorted_by_key(|t| AsRef::<str>::as_ref(t.id()))
2530 .map(Template::to_cedar)
2531 .collect_vec();
2532
2533 Some(StringifiedPolicySet {
2534 policies,
2535 policy_templates,
2536 })
2537 }
2538
2539 /// Create a fresh empty `PolicySet`
2540 pub fn new() -> Self {
2541 Self {
2542 ast: ast::PolicySet::new(),
2543 policies: LinkedHashMap::new(),
2544 templates: LinkedHashMap::new(),
2545 }
2546 }
2547
2548 /// Create a `PolicySet` from the given policies
2549 pub fn from_policies(
2550 policies: impl IntoIterator<Item = Policy>,
2551 ) -> Result<Self, PolicySetError> {
2552 let mut set = Self::new();
2553 for policy in policies {
2554 set.add(policy)?;
2555 }
2556 Ok(set)
2557 }
2558
2559 /// Helper function for `merge_policyset`
2560 /// Merges two sets and avoids name clashes by using the provided
2561 /// renaming. The type parameter `T` allows this code to be used for
2562 /// both Templates and Policies.
2563 fn merge_sets<T>(
2564 this: &mut LinkedHashMap<PolicyId, T>,
2565 other: &LinkedHashMap<PolicyId, T>,
2566 renaming: &HashMap<PolicyId, PolicyId>,
2567 ) where
2568 T: PartialEq + Clone,
2569 {
2570 for (pid, ot) in other {
2571 match renaming.get(pid) {
2572 Some(new_pid) => {
2573 this.insert(new_pid.clone(), ot.clone());
2574 }
2575 None => {
2576 if this.get(pid).is_none() {
2577 this.insert(pid.clone(), ot.clone());
2578 }
2579 // If pid is not in the renaming but is in both
2580 // this and other, then by assumption
2581 // the element at pid in this and other are equal
2582 // i.e., the renaming is expected to track all
2583 // conflicting pids.
2584 }
2585 }
2586 }
2587 }
2588
2589 /// Merges this `PolicySet` with another `PolicySet`.
2590 /// This `PolicySet` is modified while the other `PolicySet`
2591 /// remains unchanged.
2592 ///
2593 /// The flag `rename_duplicates` controls the expected behavior
2594 /// when a `PolicyId` in this and the other `PolicySet` conflict.
2595 ///
2596 /// When `rename_duplicates` is false, conflicting `PolicyId`s result
2597 /// in a `PolicySetError::AlreadyDefined` error.
2598 ///
2599 /// Otherwise, when `rename_duplicates` is true, conflicting `PolicyId`s from
2600 /// the other `PolicySet` are automatically renamed to avoid conflict.
2601 /// This renaming is returned as a Hashmap from the old `PolicyId` to the
2602 /// renamed `PolicyId`.
2603 pub fn merge(
2604 &mut self,
2605 other: &Self,
2606 rename_duplicates: bool,
2607 ) -> Result<HashMap<PolicyId, PolicyId>, PolicySetError> {
2608 match self.ast.merge_policyset(&other.ast, rename_duplicates) {
2609 Ok(renaming) => {
2610 let renaming: HashMap<PolicyId, PolicyId> = renaming
2611 .into_iter()
2612 .map(|(old_pid, new_pid)| (PolicyId::new(old_pid), PolicyId::new(new_pid)))
2613 .collect();
2614 Self::merge_sets(&mut self.templates, &other.templates, &renaming);
2615 Self::merge_sets(&mut self.policies, &other.policies, &renaming);
2616 Ok(renaming)
2617 }
2618 Err(ast::PolicySetError::Occupied { id }) => Err(PolicySetError::AlreadyDefined(
2619 policy_set_errors::AlreadyDefined {
2620 id: PolicyId::new(id),
2621 },
2622 )),
2623 }
2624 }
2625
2626 /// Add an static policy to the `PolicySet`. To add a template instance, use
2627 /// `link` instead. This function will return an error (and not modify
2628 /// the `PolicySet`) if a template-linked policy is passed in.
2629 pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
2630 if policy.is_static() {
2631 let id = PolicyId::new(policy.ast.id().clone());
2632 self.ast.add(policy.ast.clone())?;
2633 self.policies.insert(id, policy);
2634 Ok(())
2635 } else {
2636 Err(PolicySetError::ExpectedStatic(
2637 policy_set_errors::ExpectedStatic::new(),
2638 ))
2639 }
2640 }
2641
2642 /// Remove a static `Policy` from the `PolicySet`.
2643 ///
2644 /// This will error if the policy is not a static policy.
2645 pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2646 let Some(policy) = self.policies.remove(&policy_id) else {
2647 return Err(PolicySetError::PolicyNonexistent(
2648 policy_set_errors::PolicyNonexistentError { policy_id },
2649 ));
2650 };
2651 if self
2652 .ast
2653 .remove_static(&ast::PolicyID::from_string(&policy_id))
2654 .is_ok()
2655 {
2656 Ok(policy)
2657 } else {
2658 //Restore self.policies
2659 self.policies.insert(policy_id.clone(), policy);
2660 Err(PolicySetError::PolicyNonexistent(
2661 policy_set_errors::PolicyNonexistentError { policy_id },
2662 ))
2663 }
2664 }
2665
2666 /// Add a `Template` to the `PolicySet`
2667 pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
2668 let id = PolicyId::new(template.ast.id().clone());
2669 self.ast.add_template(template.ast.clone())?;
2670 self.templates.insert(id, template);
2671 Ok(())
2672 }
2673
2674 /// Remove a `Template` from the `PolicySet`.
2675 ///
2676 /// This will error if any policy is linked to the template.
2677 /// This will error if `policy_id` is not a template.
2678 pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
2679 let Some(template) = self.templates.remove(&template_id) else {
2680 return Err(PolicySetError::TemplateNonexistent(
2681 policy_set_errors::TemplateNonexistentError { template_id },
2682 ));
2683 };
2684 // If self.templates and self.ast disagree, authorization cannot be trusted.
2685 // PANIC SAFETY: We just found the policy in self.templates.
2686 #[allow(clippy::panic)]
2687 match self
2688 .ast
2689 .remove_template(&ast::PolicyID::from_string(&template_id))
2690 {
2691 Ok(_) => Ok(template),
2692 Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
2693 self.templates.insert(template_id.clone(), template);
2694 Err(PolicySetError::RemoveTemplateWithActiveLinks(
2695 policy_set_errors::RemoveTemplateWithActiveLinksError { template_id },
2696 ))
2697 }
2698 Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
2699 self.templates.insert(template_id.clone(), template);
2700 Err(PolicySetError::RemoveTemplateNotTemplate(
2701 policy_set_errors::RemoveTemplateNotTemplateError { template_id },
2702 ))
2703 }
2704 Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
2705 panic!("Found template policy in self.templates but not in self.ast");
2706 }
2707 }
2708 }
2709
2710 /// Get policies linked to a `Template` in the `PolicySet`.
2711 /// If any policy is linked to the template, this will error
2712 pub fn get_linked_policies(
2713 &self,
2714 template_id: PolicyId,
2715 ) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
2716 self.ast
2717 .get_linked_policies(&ast::PolicyID::from_string(&template_id))
2718 .map_or_else(
2719 |_| {
2720 Err(PolicySetError::TemplateNonexistent(
2721 policy_set_errors::TemplateNonexistentError { template_id },
2722 ))
2723 },
2724 |v| Ok(v.map(PolicyId::ref_cast)),
2725 )
2726 }
2727
2728 /// Iterate over all the `Policy`s in the `PolicySet`.
2729 ///
2730 /// This will include both static and template-linked policies.
2731 pub fn policies(&self) -> impl Iterator<Item = &Policy> {
2732 self.policies.values()
2733 }
2734
2735 /// Iterate over the `Template`'s in the `PolicySet`.
2736 pub fn templates(&self) -> impl Iterator<Item = &Template> {
2737 self.templates.values()
2738 }
2739
2740 /// Get a `Template` by its `PolicyId`
2741 pub fn template(&self, id: &PolicyId) -> Option<&Template> {
2742 self.templates.get(id)
2743 }
2744
2745 /// Get a `Policy` by its `PolicyId`
2746 pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
2747 self.policies.get(id)
2748 }
2749
2750 /// Extract annotation data from a `Policy` by its `PolicyId` and annotation key.
2751 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2752 /// then this function returns `Some("")`. It returns `None` only when the
2753 /// annotation is not present.
2754 pub fn annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2755 self.ast
2756 .get(id.as_ref())?
2757 .annotation(&key.as_ref().parse().ok()?)
2758 .map(AsRef::as_ref)
2759 }
2760
2761 /// Extract annotation data from a `Template` by its `PolicyId` and annotation key.
2762 /// If the annotation is present without an explicit value (e.g., `@annotation`),
2763 /// then this function returns `Some("")`. It returns `None` only when the
2764 /// annotation is not present.
2765 pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<&str> {
2766 self.ast
2767 .get_template(id.as_ref())?
2768 .annotation(&key.as_ref().parse().ok()?)
2769 .map(AsRef::as_ref)
2770 }
2771
2772 /// Returns true iff the `PolicySet` is empty
2773 pub fn is_empty(&self) -> bool {
2774 debug_assert_eq!(
2775 self.ast.is_empty(),
2776 self.policies.is_empty() && self.templates.is_empty()
2777 );
2778 self.ast.is_empty()
2779 }
2780
2781 /// Returns the number of `Policy`s in the `PolicySet`.
2782 ///
2783 /// This will include both static and template-linked policies.
2784 pub fn num_of_policies(&self) -> usize {
2785 self.policies.len()
2786 }
2787
2788 /// Returns the number of `Template`s in the `PolicySet`.
2789 pub fn num_of_templates(&self) -> usize {
2790 self.templates.len()
2791 }
2792
2793 /// Attempt to link a template and add the new template-linked policy to the policy set.
2794 /// If link fails, the `PolicySet` is not modified.
2795 /// Failure can happen for three reasons
2796 /// 1) The map passed in `vals` may not match the slots in the template
2797 /// 2) The `new_id` may conflict w/ a policy that already exists in the set
2798 /// 3) `template_id` does not correspond to a template. Either the id is
2799 /// not in the policy set, or it is in the policy set but is either a
2800 /// linked or static policy rather than a template
2801 #[allow(clippy::needless_pass_by_value)]
2802 pub fn link(
2803 &mut self,
2804 template_id: PolicyId,
2805 new_id: PolicyId,
2806 vals: HashMap<SlotId, EntityUid>,
2807 ) -> Result<(), PolicySetError> {
2808 let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
2809 .into_iter()
2810 .map(|(key, value)| (key.into(), value.into()))
2811 .collect();
2812
2813 // Try to get the template with the id we're linking from. We do this
2814 // _before_ calling `self.ast.link` because `link` mutates the policy
2815 // set by creating a new link entry in a hashmap. This happens even when
2816 // trying to link a static policy, which we want to error on here.
2817 let Some(template) = self.templates.get(&template_id) else {
2818 return Err(if self.policies.contains_key(&template_id) {
2819 policy_set_errors::ExpectedTemplate::new().into()
2820 } else {
2821 policy_set_errors::LinkingError {
2822 inner: ast::LinkingError::NoSuchTemplate {
2823 id: template_id.into(),
2824 },
2825 }
2826 .into()
2827 });
2828 };
2829
2830 let linked_ast = self.ast.link(
2831 template_id.into(),
2832 new_id.clone().into(),
2833 unwrapped_vals.clone(),
2834 )?;
2835
2836 // PANIC SAFETY: `lossless.link()` will not fail after `ast.link()` succeeds
2837 #[allow(clippy::expect_used)]
2838 let linked_lossless = template
2839 .lossless
2840 .clone()
2841 .link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
2842 // The only error case for `lossless.link()` is a template with
2843 // slots which are not filled by the provided values. `ast.link()`
2844 // will have already errored if there are any unfilled slots in the
2845 // template.
2846 .expect("ast.link() didn't fail above, so this shouldn't fail");
2847 self.policies.insert(
2848 new_id,
2849 Policy {
2850 ast: linked_ast.clone(),
2851 lossless: linked_lossless,
2852 },
2853 );
2854 Ok(())
2855 }
2856
2857 /// Get all the unknown entities from the policy set
2858 #[doc = include_str!("../experimental_warning.md")]
2859 #[cfg(feature = "partial-eval")]
2860 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
2861 let mut entity_uids = HashSet::new();
2862 for policy in self.policies.values() {
2863 entity_uids.extend(policy.unknown_entities());
2864 }
2865 entity_uids
2866 }
2867
2868 /// Unlink a template-linked policy from the policy set.
2869 /// Returns the policy that was unlinked.
2870 pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
2871 let Some(policy) = self.policies.remove(&policy_id) else {
2872 return Err(PolicySetError::LinkNonexistent(
2873 policy_set_errors::LinkNonexistentError { policy_id },
2874 ));
2875 };
2876 // If self.policies and self.ast disagree, authorization cannot be trusted.
2877 // PANIC SAFETY: We just found the policy in self.policies.
2878 #[allow(clippy::panic)]
2879 match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
2880 Ok(_) => Ok(policy),
2881 Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
2882 //Restore self.policies
2883 self.policies.insert(policy_id.clone(), policy);
2884 Err(PolicySetError::UnlinkLinkNotLink(
2885 policy_set_errors::UnlinkLinkNotLinkError { policy_id },
2886 ))
2887 }
2888 Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
2889 panic!("Found linked policy in self.policies but not in self.ast")
2890 }
2891 }
2892 }
2893}
2894
2895impl std::fmt::Display for PolicySet {
2896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2897 // prefer to display the lossless format
2898 let mut policies = self.policies().peekable();
2899 while let Some(policy) = policies.next() {
2900 policy.lossless.fmt(|| policy.ast.clone().into(), f)?;
2901 if policies.peek().is_some() {
2902 writeln!(f)?;
2903 }
2904 }
2905 Ok(())
2906 }
2907}
2908
2909/// Given a [`PolicyId`] and a [`Policy`], determine if the policy represents a static policy or a
2910/// link
2911fn is_static_or_link(
2912 (id, policy): (PolicyId, Policy),
2913) -> Result<Either<(ast::PolicyID, est::Policy), TemplateLink>, PolicyToJsonError> {
2914 match policy.template_id() {
2915 Some(template_id) => {
2916 let values = policy
2917 .ast
2918 .env()
2919 .iter()
2920 .map(|(id, euid)| (*id, euid.clone()))
2921 .collect();
2922 Ok(Either::Right(TemplateLink {
2923 new_id: id.into(),
2924 template_id: template_id.clone().into(),
2925 values,
2926 }))
2927 }
2928 None => policy
2929 .lossless
2930 .est(|| policy.ast.clone().into())
2931 .map(|est| Either::Left((id.into(), est))),
2932 }
2933}
2934
2935/// Like [`itertools::Itertools::partition_map`], but accepts a function that can fail.
2936/// The first invocation of `f` that fails causes the whole computation to fail
2937#[allow(clippy::redundant_pub_crate)] // can't be private because it's used in tests
2938pub(crate) fn fold_partition<T, A, B, E>(
2939 i: impl IntoIterator<Item = T>,
2940 f: impl Fn(T) -> Result<Either<A, B>, E>,
2941) -> Result<(Vec<A>, Vec<B>), E> {
2942 let mut lefts = vec![];
2943 let mut rights = vec![];
2944
2945 for item in i {
2946 match f(item)? {
2947 Either::Left(left) => lefts.push(left),
2948 Either::Right(right) => rights.push(right),
2949 }
2950 }
2951
2952 Ok((lefts, rights))
2953}
2954
2955/// The "type" of a [`Request`], i.e., the [`EntityTypeName`]s of principal
2956/// and resource, the [`EntityUid`] of action, and [`Option<EntityTypeName>`]s
2957/// of principal slot and resource slot
2958#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
2959pub struct RequestEnv {
2960 pub(crate) principal: EntityTypeName,
2961 pub(crate) action: EntityUid,
2962 pub(crate) resource: EntityTypeName,
2963 pub(crate) principal_slot: Option<EntityTypeName>,
2964 pub(crate) resource_slot: Option<EntityTypeName>,
2965}
2966
2967impl RequestEnv {
2968 /// Construct a [`RequestEnv`]
2969 pub fn new(principal: EntityTypeName, action: EntityUid, resource: EntityTypeName) -> Self {
2970 Self {
2971 principal,
2972 action,
2973 resource,
2974 principal_slot: None,
2975 resource_slot: None,
2976 }
2977 }
2978
2979 /// Construct a [`RequestEnv`] that contains slots in the scope
2980 pub fn new_request_env_with_slots(
2981 principal: EntityTypeName,
2982 action: EntityUid,
2983 resource: EntityTypeName,
2984 principal_slot: Option<EntityTypeName>,
2985 resource_slot: Option<EntityTypeName>,
2986 ) -> Self {
2987 Self {
2988 principal,
2989 action,
2990 resource,
2991 principal_slot,
2992 resource_slot,
2993 }
2994 }
2995
2996 /// Get the principal type name
2997 pub fn principal(&self) -> &EntityTypeName {
2998 &self.principal
2999 }
3000
3001 /// Get the action [`EntityUid`]
3002 pub fn action(&self) -> &EntityUid {
3003 &self.action
3004 }
3005
3006 /// Get the resource type name
3007 pub fn resource(&self) -> &EntityTypeName {
3008 &self.resource
3009 }
3010
3011 /// Get the principal slot type name
3012 pub fn principal_slot(&self) -> Option<&EntityTypeName> {
3013 self.principal_slot.as_ref()
3014 }
3015
3016 /// Get the resource slot type name
3017 pub fn resource_slot(&self) -> Option<&EntityTypeName> {
3018 self.resource_slot.as_ref()
3019 }
3020}
3021
3022#[doc(hidden)]
3023impl From<cedar_policy_core::validator::types::RequestEnv<'_>> for RequestEnv {
3024 fn from(renv: cedar_policy_core::validator::types::RequestEnv<'_>) -> Self {
3025 match renv {
3026 cedar_policy_core::validator::types::RequestEnv::DeclaredAction {
3027 principal,
3028 action,
3029 resource,
3030 principal_slot,
3031 resource_slot,
3032 ..
3033 } => Self {
3034 principal: principal.clone().into(),
3035 action: action.clone().into(),
3036 resource: resource.clone().into(),
3037 principal_slot: principal_slot.map(EntityTypeName::from),
3038 resource_slot: resource_slot.map(EntityTypeName::from),
3039 },
3040 // PANIC SAFETY: partial validation is not enabled and hence `RequestEnv::UndeclaredAction` should not show up
3041 #[allow(clippy::unreachable)]
3042 cedar_policy_core::validator::types::RequestEnv::UndeclaredAction => {
3043 unreachable!("used unsupported feature")
3044 }
3045 }
3046 }
3047}
3048
3049/// Get valid request envs for an `ast::Template`
3050///
3051/// This function is called by [`Template::get_valid_request_envs`] and
3052/// [`Policy::get_valid_request_envs`]
3053fn get_valid_request_envs(ast: &ast::Template, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3054 let tc = Typechecker::new(
3055 &s.0,
3056 cedar_policy_core::validator::ValidationMode::default(),
3057 );
3058 tc.typecheck_by_request_env(ast)
3059 .into_iter()
3060 .filter_map(|(env, pc)| {
3061 if matches!(pc, PolicyCheck::Success(_)) {
3062 Some(env.into())
3063 } else {
3064 None
3065 }
3066 })
3067 .collect::<BTreeSet<_>>()
3068 .into_iter()
3069}
3070
3071/// Policy template datatype
3072//
3073// NOTE: Unlike the internal type [`ast::Template`], this type only supports
3074// templates. The `Template` constructors will return an error if provided with
3075// a static policy.
3076#[derive(Debug, Clone)]
3077pub struct Template {
3078 /// AST representation of the template, used for most operations.
3079 /// In particular, the `ast` contains the authoritative `PolicyId` for the template.
3080 pub(crate) ast: ast::Template,
3081
3082 /// Some "lossless" representation of the template, whichever is most
3083 /// convenient to provide (and can be provided with the least overhead).
3084 /// This is used just for `to_json()`.
3085 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3086 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3087 /// we can from the EST (modulo whitespace and a few other things like the
3088 /// order of annotations).
3089 ///
3090 /// This is a `LosslessPolicy` (rather than something like `LosslessTemplate`)
3091 /// because the EST doesn't distinguish between static policies and templates.
3092 pub(crate) lossless: LosslessPolicy,
3093}
3094
3095impl PartialEq for Template {
3096 fn eq(&self, other: &Self) -> bool {
3097 // eq is based on just the `ast`
3098 self.ast.eq(&other.ast)
3099 }
3100}
3101impl Eq for Template {}
3102
3103#[doc(hidden)] // because this converts to a private/internal type
3104impl AsRef<ast::Template> for Template {
3105 fn as_ref(&self) -> &ast::Template {
3106 &self.ast
3107 }
3108}
3109
3110#[doc(hidden)]
3111impl From<ast::Template> for Template {
3112 fn from(template: ast::Template) -> Self {
3113 Self::from_ast(template)
3114 }
3115}
3116
3117impl Template {
3118 /// Attempt to parse a [`Template`] from source.
3119 /// Returns an error if the input is a static policy (i.e., has no slots).
3120 /// If `id` is Some, then the resulting template will have that `id`.
3121 /// If the `id` is None, the parser will use the default "policy0".
3122 /// The behavior around None may change in the future.
3123 pub fn parse(id: Option<PolicyId>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3124 let ast = parser::parse_template(id.map(Into::into), src.as_ref())?;
3125 Ok(Self {
3126 ast,
3127 lossless: LosslessPolicy::policy_or_template_text(Some(src.as_ref())),
3128 })
3129 }
3130
3131 /// Get the `PolicyId` of this `Template`
3132 pub fn id(&self) -> &PolicyId {
3133 PolicyId::ref_cast(self.ast.id())
3134 }
3135
3136 /// Clone this `Template` with a new `PolicyId`
3137 #[must_use]
3138 pub fn new_id(&self, id: PolicyId) -> Self {
3139 Self {
3140 ast: self.ast.new_id(id.into()),
3141 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3142 }
3143 }
3144
3145 /// Get the `Effect` (`Forbid` or `Permit`) of this `Template`
3146 pub fn effect(&self) -> Effect {
3147 self.ast.effect()
3148 }
3149
3150 /// Returns `true` if this template has a `when` or `unless` clause.
3151 pub fn has_non_scope_constraint(&self) -> bool {
3152 self.ast.non_scope_constraints().is_some()
3153 }
3154
3155 /// Get an annotation value of this `Template`.
3156 /// If the annotation is present without an explicit value (e.g., `@annotation`),
3157 /// then this function returns `Some("")`. Returns `None` when the
3158 /// annotation is not present or when `key` is not a valid annotation identifier.
3159 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3160 self.ast
3161 .annotation(&key.as_ref().parse().ok()?)
3162 .map(AsRef::as_ref)
3163 }
3164
3165 /// Iterate through annotation data of this `Template` as key-value pairs.
3166 /// Annotations which do not have an explicit value (e.g., `@annotation`),
3167 /// are included in the iterator with the value `""`.
3168 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3169 self.ast
3170 .annotations()
3171 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3172 }
3173
3174 /// Iterate over the open slots in this `Template`
3175 pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
3176 self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
3177 }
3178
3179 /// Get the scope constraint on this policy's principal
3180 pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
3181 match self.ast.principal_constraint().as_inner() {
3182 ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
3183 ast::PrincipalOrResourceConstraint::In(eref) => {
3184 TemplatePrincipalConstraint::In(match eref {
3185 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3186 ast::EntityReference::Slot(_) => None,
3187 })
3188 }
3189 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3190 TemplatePrincipalConstraint::Eq(match eref {
3191 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3192 ast::EntityReference::Slot(_) => None,
3193 })
3194 }
3195 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3196 TemplatePrincipalConstraint::Is(entity_type.as_ref().clone().into())
3197 }
3198 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3199 TemplatePrincipalConstraint::IsIn(
3200 entity_type.as_ref().clone().into(),
3201 match eref {
3202 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3203 ast::EntityReference::Slot(_) => None,
3204 },
3205 )
3206 }
3207 }
3208 }
3209
3210 /// Get the scope constraint on this policy's action
3211 pub fn action_constraint(&self) -> ActionConstraint {
3212 // Clone the data from Core to be consistent with the other constraints
3213 match self.ast.action_constraint() {
3214 ast::ActionConstraint::Any => ActionConstraint::Any,
3215 ast::ActionConstraint::In(ids) => {
3216 ActionConstraint::In(ids.iter().map(|id| id.as_ref().clone().into()).collect())
3217 }
3218 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(id.as_ref().clone().into()),
3219 #[cfg(feature = "tolerant-ast")]
3220 ast::ActionConstraint::ErrorConstraint => {
3221 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3222 // It is not recommended to evaluate an AST that allows error nodes
3223 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3224 // treat it as `Any`
3225 ActionConstraint::Any
3226 }
3227 }
3228 }
3229
3230 /// Get the scope constraint on this policy's resource
3231 pub fn resource_constraint(&self) -> TemplateResourceConstraint {
3232 match self.ast.resource_constraint().as_inner() {
3233 ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
3234 ast::PrincipalOrResourceConstraint::In(eref) => {
3235 TemplateResourceConstraint::In(match eref {
3236 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3237 ast::EntityReference::Slot(_) => None,
3238 })
3239 }
3240 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3241 TemplateResourceConstraint::Eq(match eref {
3242 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3243 ast::EntityReference::Slot(_) => None,
3244 })
3245 }
3246 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3247 TemplateResourceConstraint::Is(entity_type.as_ref().clone().into())
3248 }
3249 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3250 TemplateResourceConstraint::IsIn(
3251 entity_type.as_ref().clone().into(),
3252 match eref {
3253 ast::EntityReference::EUID(e) => Some(e.as_ref().clone().into()),
3254 ast::EntityReference::Slot(_) => None,
3255 },
3256 )
3257 }
3258 }
3259 }
3260
3261 /// Create a [`Template`] from its JSON representation.
3262 /// Returns an error if the input is a static policy (i.e., has no slots).
3263 /// If `id` is Some, the policy will be given that Policy Id.
3264 /// If `id` is None, then "JSON policy" will be used.
3265 /// The behavior around None may change in the future.
3266 pub fn from_json(
3267 id: Option<PolicyId>,
3268 json: serde_json::Value,
3269 ) -> Result<Self, PolicyFromJsonError> {
3270 let est: est::Policy = serde_json::from_value(json)
3271 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3272 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3273 Self::from_est(id, est)
3274 }
3275
3276 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3277 Ok(Self {
3278 ast: est.clone().try_into_ast_template(id.map(PolicyId::into))?,
3279 lossless: LosslessPolicy::Est(est),
3280 })
3281 }
3282
3283 #[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
3284 pub(crate) fn from_ast(ast: ast::Template) -> Self {
3285 Self {
3286 lossless: LosslessPolicy::Est(ast.clone().into()),
3287 ast,
3288 }
3289 }
3290
3291 /// Get the JSON representation of this `Template`.
3292 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3293 let est = self.lossless.est(|| self.ast.clone().into())?;
3294 serde_json::to_value(est).map_err(Into::into)
3295 }
3296
3297 /// Get the human-readable Cedar syntax representation of this template.
3298 /// This function is primarily intended for rendering JSON policies in the
3299 /// human-readable syntax, but it will also return the original policy text
3300 /// when given a policy parsed from the human-readable syntax.
3301 ///
3302 /// It also does not format the policy according to any particular rules.
3303 /// Policy formatting can be done through the Cedar policy CLI or
3304 /// the `cedar-policy-formatter` crate.
3305 pub fn to_cedar(&self) -> String {
3306 match &self.lossless {
3307 LosslessPolicy::Empty | LosslessPolicy::Est(_) => self.ast.to_string(),
3308 LosslessPolicy::Text { text, .. } => text.clone(),
3309 }
3310 }
3311
3312 /// Get the valid [`RequestEnv`]s for this template, according to the schema.
3313 ///
3314 /// That is, all the [`RequestEnv`]s in the schema for which this template is
3315 /// not trivially false.
3316 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3317 get_valid_request_envs(&self.ast, s)
3318 }
3319}
3320
3321impl std::fmt::Display for Template {
3322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3323 // prefer to display the lossless format
3324 self.lossless.fmt(|| self.ast.clone().into(), f)
3325 }
3326}
3327
3328impl FromStr for Template {
3329 type Err = ParseErrors;
3330
3331 fn from_str(src: &str) -> Result<Self, Self::Err> {
3332 Self::parse(None, src)
3333 }
3334}
3335
3336/// Scope constraint on policy principals.
3337#[derive(Debug, Clone, PartialEq, Eq)]
3338pub enum PrincipalConstraint {
3339 /// Un-constrained
3340 Any,
3341 /// Must be In the given [`EntityUid`]
3342 In(EntityUid),
3343 /// Must be equal to the given [`EntityUid`]
3344 Eq(EntityUid),
3345 /// Must be the given [`EntityTypeName`]
3346 Is(EntityTypeName),
3347 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3348 IsIn(EntityTypeName, EntityUid),
3349}
3350
3351/// Scope constraint on policy principals for templates.
3352#[derive(Debug, Clone, PartialEq, Eq)]
3353pub enum TemplatePrincipalConstraint {
3354 /// Un-constrained
3355 Any,
3356 /// Must be In the given [`EntityUid`].
3357 /// If [`None`], then it is a template slot.
3358 In(Option<EntityUid>),
3359 /// Must be equal to the given [`EntityUid`].
3360 /// If [`None`], then it is a template slot.
3361 Eq(Option<EntityUid>),
3362 /// Must be the given [`EntityTypeName`].
3363 Is(EntityTypeName),
3364 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3365 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3366 IsIn(EntityTypeName, Option<EntityUid>),
3367}
3368
3369impl TemplatePrincipalConstraint {
3370 /// Does this constraint contain a slot?
3371 pub fn has_slot(&self) -> bool {
3372 match self {
3373 Self::Any | Self::Is(_) => false,
3374 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3375 }
3376 }
3377}
3378
3379/// Scope constraint on policy actions.
3380#[derive(Debug, Clone, PartialEq, Eq)]
3381pub enum ActionConstraint {
3382 /// Un-constrained
3383 Any,
3384 /// Must be In the given [`EntityUid`]
3385 In(Vec<EntityUid>),
3386 /// Must be equal to the given [`EntityUid]`
3387 Eq(EntityUid),
3388}
3389
3390/// Scope constraint on policy resources.
3391#[derive(Debug, Clone, PartialEq, Eq)]
3392pub enum ResourceConstraint {
3393 /// Un-constrained
3394 Any,
3395 /// Must be In the given [`EntityUid`]
3396 In(EntityUid),
3397 /// Must be equal to the given [`EntityUid`]
3398 Eq(EntityUid),
3399 /// Must be the given [`EntityTypeName`]
3400 Is(EntityTypeName),
3401 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`]
3402 IsIn(EntityTypeName, EntityUid),
3403}
3404
3405/// Scope constraint on policy resources for templates.
3406#[derive(Debug, Clone, PartialEq, Eq)]
3407pub enum TemplateResourceConstraint {
3408 /// Un-constrained
3409 Any,
3410 /// Must be In the given [`EntityUid`].
3411 /// If [`None`], then it is a template slot.
3412 In(Option<EntityUid>),
3413 /// Must be equal to the given [`EntityUid`].
3414 /// If [`None`], then it is a template slot.
3415 Eq(Option<EntityUid>),
3416 /// Must be the given [`EntityTypeName`].
3417 Is(EntityTypeName),
3418 /// Must be the given [`EntityTypeName`], and `in` the [`EntityUid`].
3419 /// If the [`EntityUid`] is [`Option::None`], then it is a template slot.
3420 IsIn(EntityTypeName, Option<EntityUid>),
3421}
3422
3423impl TemplateResourceConstraint {
3424 /// Does this constraint contain a slot?
3425 pub fn has_slot(&self) -> bool {
3426 match self {
3427 Self::Any | Self::Is(_) => false,
3428 Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
3429 }
3430 }
3431}
3432
3433/// Structure for a `Policy`. Includes both static policies and template-linked policies.
3434#[derive(Debug, Clone)]
3435pub struct Policy {
3436 /// AST representation of the policy, used for most operations.
3437 /// In particular, the `ast` contains the authoritative `PolicyId` for the policy.
3438 pub(crate) ast: ast::Policy,
3439 /// Some "lossless" representation of the policy, whichever is most
3440 /// convenient to provide (and can be provided with the least overhead).
3441 /// This is used just for `to_json()`.
3442 /// We can't just derive this on-demand from `ast`, because the AST is lossy:
3443 /// we can't reconstruct an accurate CST/EST/policy-text from the AST, but
3444 /// we can from the EST (modulo whitespace and a few other things like the
3445 /// order of annotations).
3446 pub(crate) lossless: LosslessPolicy,
3447}
3448
3449impl PartialEq for Policy {
3450 fn eq(&self, other: &Self) -> bool {
3451 // eq is based on just the `ast`
3452 self.ast.eq(&other.ast)
3453 }
3454}
3455impl Eq for Policy {}
3456
3457#[doc(hidden)] // because this converts to a private/internal type
3458impl AsRef<ast::Policy> for Policy {
3459 fn as_ref(&self) -> &ast::Policy {
3460 &self.ast
3461 }
3462}
3463
3464#[doc(hidden)]
3465impl From<ast::Policy> for Policy {
3466 fn from(policy: ast::Policy) -> Self {
3467 Self::from_ast(policy)
3468 }
3469}
3470
3471#[doc(hidden)]
3472impl From<ast::StaticPolicy> for Policy {
3473 fn from(policy: ast::StaticPolicy) -> Self {
3474 ast::Policy::from(policy).into()
3475 }
3476}
3477
3478impl Policy {
3479 /// Get the `PolicyId` of the `Template` this is linked to.
3480 /// If this is a static policy, this will return `None`.
3481 pub fn template_id(&self) -> Option<&PolicyId> {
3482 if self.is_static() {
3483 None
3484 } else {
3485 Some(PolicyId::ref_cast(self.ast.template().id()))
3486 }
3487 }
3488
3489 /// Get the values this `Template` is linked to, expressed as a map from `SlotId` to `EntityUid`.
3490 /// If this is a static policy, this will return `None`.
3491 pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
3492 if self.is_static() {
3493 None
3494 } else {
3495 let wrapped_vals: HashMap<SlotId, EntityUid> = self
3496 .ast
3497 .env()
3498 .iter()
3499 .map(|(key, value)| ((*key).into(), value.clone().into()))
3500 .collect();
3501 Some(wrapped_vals)
3502 }
3503 }
3504
3505 /// Get the `Effect` (`Permit` or `Forbid`) for this instance
3506 pub fn effect(&self) -> Effect {
3507 self.ast.effect()
3508 }
3509
3510 /// Returns `true` if this policy has a `when` or `unless` clause.
3511 pub fn has_non_scope_constraint(&self) -> bool {
3512 self.ast.non_scope_constraints().is_some()
3513 }
3514
3515 /// Get an annotation value of this template-linked or static policy.
3516 /// If the annotation is present without an explicit value (e.g., `@annotation`),
3517 /// then this function returns `Some("")`. Returns `None` when the
3518 /// annotation is not present or when `key` is not a valid annotations identifier.
3519 pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
3520 self.ast
3521 .annotation(&key.as_ref().parse().ok()?)
3522 .map(AsRef::as_ref)
3523 }
3524
3525 /// Iterate through annotation data of this template-linked or static policy.
3526 /// Annotations which do not have an explicit value (e.g., `@annotation`),
3527 /// are included in the iterator with the value `""`.
3528 pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
3529 self.ast
3530 .annotations()
3531 .map(|(k, v)| (k.as_ref(), v.as_ref()))
3532 }
3533
3534 /// Get the `PolicyId` for this template-linked or static policy
3535 pub fn id(&self) -> &PolicyId {
3536 PolicyId::ref_cast(self.ast.id())
3537 }
3538
3539 /// Clone this `Policy` with a new `PolicyId`
3540 #[must_use]
3541 pub fn new_id(&self, id: PolicyId) -> Self {
3542 Self {
3543 ast: self.ast.new_id(id.into()),
3544 lossless: self.lossless.clone(), // Lossless representation doesn't include the `PolicyId`
3545 }
3546 }
3547
3548 /// Returns `true` if this is a static policy, `false` otherwise.
3549 pub fn is_static(&self) -> bool {
3550 self.ast.is_static()
3551 }
3552
3553 /// Get the scope constraint on this policy's principal
3554 pub fn principal_constraint(&self) -> PrincipalConstraint {
3555 let slot_id = ast::SlotId::principal();
3556 match self.ast.template().principal_constraint().as_inner() {
3557 ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
3558 ast::PrincipalOrResourceConstraint::In(eref) => {
3559 PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3560 }
3561 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3562 PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3563 }
3564 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3565 PrincipalConstraint::Is(entity_type.as_ref().clone().into())
3566 }
3567 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3568 PrincipalConstraint::IsIn(
3569 entity_type.as_ref().clone().into(),
3570 self.convert_entity_reference(eref, slot_id).clone(),
3571 )
3572 }
3573 }
3574 }
3575
3576 /// Get the scope constraint on this policy's action
3577 pub fn action_constraint(&self) -> ActionConstraint {
3578 // Clone the data from Core to be consistant with the other constraints
3579 match self.ast.template().action_constraint() {
3580 ast::ActionConstraint::Any => ActionConstraint::Any,
3581 ast::ActionConstraint::In(ids) => ActionConstraint::In(
3582 ids.iter()
3583 .map(|euid| EntityUid::ref_cast(euid.as_ref()))
3584 .cloned()
3585 .collect(),
3586 ),
3587 ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
3588 #[cfg(feature = "tolerant-ast")]
3589 ast::ActionConstraint::ErrorConstraint => {
3590 // We will only have an ErrorConstraint if we are using a parser that allows Error nodes
3591 // It is not recommended to evaluate an AST that allows error nodes
3592 // If somehow someone tries to evaluate an AST that includes an Action constraint error, we will
3593 // treat it as `Any`
3594 ActionConstraint::Any
3595 }
3596 }
3597 }
3598
3599 /// Get the scope constraint on this policy's resource
3600 pub fn resource_constraint(&self) -> ResourceConstraint {
3601 let slot_id = ast::SlotId::resource();
3602 match self.ast.template().resource_constraint().as_inner() {
3603 ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
3604 ast::PrincipalOrResourceConstraint::In(eref) => {
3605 ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
3606 }
3607 ast::PrincipalOrResourceConstraint::Eq(eref) => {
3608 ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
3609 }
3610 ast::PrincipalOrResourceConstraint::Is(entity_type) => {
3611 ResourceConstraint::Is(entity_type.as_ref().clone().into())
3612 }
3613 ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
3614 ResourceConstraint::IsIn(
3615 entity_type.as_ref().clone().into(),
3616 self.convert_entity_reference(eref, slot_id).clone(),
3617 )
3618 }
3619 }
3620 }
3621
3622 /// To avoid panicking, this function may only be called when `slot` is the
3623 /// `SlotId` corresponding to the scope constraint from which the entity
3624 /// reference `r` was extracted. I.e., If `r` is taken from the principal
3625 /// scope constraint, `slot` must be `?principal`. This ensures that the
3626 /// `SlotId` exists in the policy (and therefore the slot environment map)
3627 /// whenever the `EntityReference` `r` is the Slot variant.
3628 fn convert_entity_reference<'a>(
3629 &'a self,
3630 r: &'a ast::EntityReference,
3631 slot: ast::SlotId,
3632 ) -> &'a EntityUid {
3633 match r {
3634 ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
3635 // PANIC SAFETY: This `unwrap` here is safe due the invariant (values total map) on policies.
3636 #[allow(clippy::unwrap_used)]
3637 ast::EntityReference::Slot(_) => {
3638 EntityUid::ref_cast(self.ast.env().get(&slot).unwrap())
3639 }
3640 }
3641 }
3642
3643 /// Parse a single policy.
3644 /// If `id` is Some, the policy will be given that Policy Id.
3645 /// If `id` is None, then "policy0" will be used.
3646 /// The behavior around None may change in the future.
3647 ///
3648 /// This can fail if the policy fails to parse.
3649 /// It can also fail if a template was passed in, as this function only accepts static
3650 /// policies
3651 pub fn parse(id: Option<PolicyId>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
3652 let inline_ast = parser::parse_policy(id.map(Into::into), policy_src.as_ref())?;
3653 let (_, ast) = ast::Template::link_static_policy(inline_ast);
3654 Ok(Self {
3655 ast,
3656 lossless: LosslessPolicy::policy_or_template_text(Some(policy_src.as_ref())),
3657 })
3658 }
3659
3660 /// Create a `Policy` from its JSON representation.
3661 /// If `id` is Some, the policy will be given that Policy Id.
3662 /// If `id` is None, then "JSON policy" will be used.
3663 /// The behavior around None may change in the future.
3664 ///
3665 /// ```
3666 /// # use cedar_policy::{Policy, PolicyId};
3667 ///
3668 /// let json: serde_json::Value = serde_json::json!(
3669 /// {
3670 /// "effect":"permit",
3671 /// "principal":{
3672 /// "op":"==",
3673 /// "entity":{
3674 /// "type":"User",
3675 /// "id":"bob"
3676 /// }
3677 /// },
3678 /// "action":{
3679 /// "op":"==",
3680 /// "entity":{
3681 /// "type":"Action",
3682 /// "id":"view"
3683 /// }
3684 /// },
3685 /// "resource":{
3686 /// "op":"==",
3687 /// "entity":{
3688 /// "type":"Album",
3689 /// "id":"trip"
3690 /// }
3691 /// },
3692 /// "conditions":[
3693 /// {
3694 /// "kind":"when",
3695 /// "body":{
3696 /// ">":{
3697 /// "left":{
3698 /// ".":{
3699 /// "left":{
3700 /// "Var":"principal"
3701 /// },
3702 /// "attr":"age"
3703 /// }
3704 /// },
3705 /// "right":{
3706 /// "Value":18
3707 /// }
3708 /// }
3709 /// }
3710 /// }
3711 /// ]
3712 /// }
3713 /// );
3714 /// let json_policy = Policy::from_json(None, json).unwrap();
3715 /// let src = r#"
3716 /// permit(
3717 /// principal == User::"bob",
3718 /// action == Action::"view",
3719 /// resource == Album::"trip"
3720 /// )
3721 /// when { principal.age > 18 };"#;
3722 /// let text_policy = Policy::parse(None, src).unwrap();
3723 /// assert_eq!(json_policy.to_json().unwrap(), text_policy.to_json().unwrap());
3724 /// ```
3725 pub fn from_json(
3726 id: Option<PolicyId>,
3727 json: serde_json::Value,
3728 ) -> Result<Self, PolicyFromJsonError> {
3729 let est: est::Policy = serde_json::from_value(json)
3730 .map_err(|e| entities_json_errors::JsonDeserializationError::Serde(e.into()))
3731 .map_err(cedar_policy_core::est::FromJsonError::from)?;
3732 Self::from_est(id, est)
3733 }
3734
3735 /// Get the valid [`RequestEnv`]s for this policy, according to the schema.
3736 ///
3737 /// That is, all the [`RequestEnv`]s in the schema for which this policy is
3738 /// not trivially false.
3739 pub fn get_valid_request_envs(&self, s: &Schema) -> impl Iterator<Item = RequestEnv> {
3740 get_valid_request_envs(self.ast.template(), s)
3741 }
3742
3743 /// Get all entity literals occuring in a `Policy`
3744 pub fn entity_literals(&self) -> Vec<EntityUid> {
3745 self.ast
3746 .condition()
3747 .subexpressions()
3748 .filter_map(|e| match e.expr_kind() {
3749 cedar_policy_core::ast::ExprKind::Lit(
3750 cedar_policy_core::ast::Literal::EntityUID(euid),
3751 ) => Some(EntityUid((*euid).as_ref().clone())),
3752 _ => None,
3753 })
3754 .collect()
3755 }
3756
3757 /// Return a new policy where all occurrences of key `EntityUid`s are replaced by value `EntityUid`
3758 /// (as a single, non-sequential substitution).
3759 pub fn sub_entity_literals(
3760 &self,
3761 mapping: BTreeMap<EntityUid, EntityUid>,
3762 ) -> Result<Self, PolicyFromJsonError> {
3763 // PANIC SAFETY: This can't fail for a policy that was already constructed
3764 #[allow(clippy::expect_used)]
3765 let cloned_est = self
3766 .lossless
3767 .est(|| self.ast.clone().into())
3768 .expect("Internal error, failed to construct est.");
3769
3770 let mapping = mapping.into_iter().map(|(k, v)| (k.0, v.0)).collect();
3771
3772 // PANIC SAFETY: This can't fail for a policy that was already constructed
3773 #[allow(clippy::expect_used)]
3774 let est = cloned_est
3775 .sub_entity_literals(&mapping)
3776 .expect("Internal error, failed to sub entity literals.");
3777
3778 let ast = match est.clone().try_into_ast_policy(Some(self.ast.id().clone())) {
3779 Ok(ast) => ast,
3780 Err(e) => return Err(e.into()),
3781 };
3782
3783 Ok(Self {
3784 ast,
3785 lossless: LosslessPolicy::Est(est),
3786 })
3787 }
3788
3789 fn from_est(id: Option<PolicyId>, est: est::Policy) -> Result<Self, PolicyFromJsonError> {
3790 Ok(Self {
3791 ast: est.clone().try_into_ast_policy(id.map(PolicyId::into))?,
3792 lossless: LosslessPolicy::Est(est),
3793 })
3794 }
3795
3796 /// Get the JSON representation of this `Policy`.
3797 /// ```
3798 /// # use cedar_policy::Policy;
3799 /// let src = r#"
3800 /// permit(
3801 /// principal == User::"bob",
3802 /// action == Action::"view",
3803 /// resource == Album::"trip"
3804 /// )
3805 /// when { principal.age > 18 };"#;
3806 ///
3807 /// let policy = Policy::parse(None, src).unwrap();
3808 /// println!("{}", policy);
3809 /// // convert the policy to JSON
3810 /// let json = policy.to_json().unwrap();
3811 /// println!("{}", json);
3812 /// assert_eq!(json, Policy::from_json(None, json.clone()).unwrap().to_json().unwrap());
3813 /// ```
3814 pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
3815 let est = self.lossless.est(|| self.ast.clone().into())?;
3816 serde_json::to_value(est).map_err(Into::into)
3817 }
3818
3819 /// Get the human-readable Cedar syntax representation of this policy. This
3820 /// function is primarily intended for rendering JSON policies in the
3821 /// human-readable syntax, but it will also return the original policy text
3822 /// when given a policy parsed from the human-readable syntax.
3823 ///
3824 /// It will return `None` for linked policies because they cannot be
3825 /// directly rendered in Cedar syntax. You can instead render the unlinked
3826 /// template if you do not need to preserve links. If serializing links is
3827 /// important, then you will need to serialize the whole policy set
3828 /// containing the template and link to JSON (or protobuf).
3829 ///
3830 /// It also does not format the policy according to any particular rules.
3831 /// Policy formatting can be done through the Cedar policy CLI or
3832 /// the `cedar-policy-formatter` crate.
3833 pub fn to_cedar(&self) -> Option<String> {
3834 match &self.lossless {
3835 LosslessPolicy::Empty | LosslessPolicy::Est(_) => Some(self.ast.to_string()),
3836 LosslessPolicy::Text { text, slots } => {
3837 if slots.is_empty() {
3838 Some(text.clone())
3839 } else {
3840 None
3841 }
3842 }
3843 }
3844 }
3845
3846 /// Get all the unknown entities from the policy
3847 #[doc = include_str!("../experimental_warning.md")]
3848 #[cfg(feature = "partial-eval")]
3849 pub fn unknown_entities(&self) -> HashSet<EntityUid> {
3850 self.ast
3851 .unknown_entities()
3852 .into_iter()
3853 .map(Into::into)
3854 .collect()
3855 }
3856
3857 /// Create a `Policy` from its AST representation only. The `LosslessPolicy`
3858 /// will reflect the AST structure. When possible, don't use this method and
3859 /// create the `Policy` from the policy text, CST, or EST instead, as the
3860 /// conversion to AST is lossy. ESTs for policies generated by this method
3861 /// will reflect the AST and not the original policy syntax.
3862 #[cfg_attr(
3863 not(any(feature = "partial-eval", feature = "protobufs")),
3864 allow(unused)
3865 )]
3866 pub(crate) fn from_ast(ast: ast::Policy) -> Self {
3867 let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
3868 Self {
3869 ast,
3870 lossless: LosslessPolicy::policy_or_template_text(Some(text)),
3871 }
3872 }
3873}
3874
3875impl std::fmt::Display for Policy {
3876 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3877 // prefer to display the lossless format
3878 self.lossless.fmt(|| self.ast.clone().into(), f)
3879 }
3880}
3881
3882impl FromStr for Policy {
3883 type Err = ParseErrors;
3884 /// Create a policy
3885 ///
3886 /// Important note: Policies have ids, but this interface does not
3887 /// allow them to be set. It will use the default "policy0", which
3888 /// may cause id conflicts if not handled. Use `Policy::parse` to set
3889 /// the id when parsing, or `Policy::new_id` to clone a policy with
3890 /// a new id.
3891 fn from_str(policy: &str) -> Result<Self, Self::Err> {
3892 Self::parse(None, policy)
3893 }
3894}
3895
3896/// See comments on `Policy` and `Template`.
3897///
3898/// This structure can be used for static policies, linked policies, and templates.
3899#[derive(Debug, Clone)]
3900pub(crate) enum LosslessPolicy {
3901 /// An empty representation
3902 Empty,
3903 /// EST representation
3904 Est(est::Policy),
3905 /// Text representation
3906 Text {
3907 /// actual policy text, of the policy or template
3908 text: String,
3909 /// For linked policies, map of slot to UID. Only linked policies have
3910 /// this; static policies and (unlinked) templates have an empty map
3911 /// here
3912 slots: HashMap<ast::SlotId, ast::EntityUID>,
3913 },
3914}
3915
3916impl LosslessPolicy {
3917 /// Create a new `LosslessPolicy` from the text of a policy or template.
3918 fn policy_or_template_text(text: Option<impl Into<String>>) -> Self {
3919 text.map_or(Self::Empty, |text| Self::Text {
3920 text: text.into(),
3921 slots: HashMap::new(),
3922 })
3923 }
3924
3925 /// Get the EST representation of this static policy, linked policy, or template.
3926 fn est(
3927 &self,
3928 fallback_est: impl FnOnce() -> est::Policy,
3929 ) -> Result<est::Policy, PolicyToJsonError> {
3930 match self {
3931 // Fall back to the `policy` AST if the lossless representation is empty
3932 Self::Empty => Ok(fallback_est()),
3933 Self::Est(est) => Ok(est.clone()),
3934 Self::Text { text, slots } => {
3935 let est =
3936 parser::parse_policy_or_template_to_est(text).map_err(ParseErrors::from)?;
3937 if slots.is_empty() {
3938 Ok(est)
3939 } else {
3940 let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
3941 Ok(est.link(&unwrapped_vals)?)
3942 }
3943 }
3944 }
3945 }
3946
3947 fn link<'a>(
3948 self,
3949 vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
3950 ) -> Result<Self, est::LinkingError> {
3951 match self {
3952 Self::Empty => Ok(Self::Empty),
3953 Self::Est(est) => {
3954 let unwrapped_est_vals: HashMap<
3955 ast::SlotId,
3956 cedar_policy_core::entities::EntityUidJson,
3957 > = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
3958 Ok(Self::Est(est.link(&unwrapped_est_vals)?))
3959 }
3960 Self::Text { text, slots } => {
3961 debug_assert!(
3962 slots.is_empty(),
3963 "shouldn't call link() on an already-linked policy"
3964 );
3965 let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
3966 Ok(Self::Text { text, slots })
3967 }
3968 }
3969 }
3970
3971 fn fmt(
3972 &self,
3973 fallback_est: impl FnOnce() -> est::Policy,
3974 f: &mut std::fmt::Formatter<'_>,
3975 ) -> std::fmt::Result {
3976 match self {
3977 Self::Empty => match self.est(fallback_est) {
3978 Ok(est) => write!(f, "{est}"),
3979 Err(e) => write!(f, "<invalid policy: {e}>"),
3980 },
3981 Self::Est(est) => write!(f, "{est}"),
3982 Self::Text { text, slots } => {
3983 if slots.is_empty() {
3984 write!(f, "{text}")
3985 } else {
3986 // need to replace placeholders according to `slots`.
3987 // just find-and-replace wouldn't be safe/perfect, we
3988 // want to use the actual parser; right now we reuse
3989 // another implementation by just converting to EST and
3990 // printing that
3991 match self.est(fallback_est) {
3992 Ok(est) => write!(f, "{est}"),
3993 Err(e) => write!(f, "<invalid linked policy: {e}>"),
3994 }
3995 }
3996 }
3997 }
3998 }
3999}
4000
4001/// Expressions to be evaluated
4002#[repr(transparent)]
4003#[derive(Debug, Clone, RefCast)]
4004pub struct Expression(pub(crate) ast::Expr);
4005
4006#[doc(hidden)] // because this converts to a private/internal type
4007impl AsRef<ast::Expr> for Expression {
4008 fn as_ref(&self) -> &ast::Expr {
4009 &self.0
4010 }
4011}
4012
4013#[doc(hidden)]
4014impl From<ast::Expr> for Expression {
4015 fn from(expr: ast::Expr) -> Self {
4016 Self(expr)
4017 }
4018}
4019
4020impl Expression {
4021 /// Create an expression representing a literal string.
4022 pub fn new_string(value: String) -> Self {
4023 Self(ast::Expr::val(value))
4024 }
4025
4026 /// Create an expression representing a literal bool.
4027 pub fn new_bool(value: bool) -> Self {
4028 Self(ast::Expr::val(value))
4029 }
4030
4031 /// Create an expression representing a literal long.
4032 pub fn new_long(value: ast::Integer) -> Self {
4033 Self(ast::Expr::val(value))
4034 }
4035
4036 /// Create an expression representing a record.
4037 ///
4038 /// Error if any key appears two or more times in `fields`.
4039 pub fn new_record(
4040 fields: impl IntoIterator<Item = (String, Self)>,
4041 ) -> Result<Self, ExpressionConstructionError> {
4042 Ok(Self(ast::Expr::record(
4043 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4044 )?))
4045 }
4046
4047 /// Create an expression representing a Set.
4048 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4049 Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
4050 }
4051
4052 /// Create an expression representing an ip address.
4053 /// This function does not perform error checking on the source string,
4054 /// it creates an expression that calls the `ip` constructor.
4055 pub fn new_ip(src: impl AsRef<str>) -> Self {
4056 let src_expr = ast::Expr::val(src.as_ref());
4057 Self(ast::Expr::call_extension_fn(
4058 ip_extension_name(),
4059 vec![src_expr],
4060 ))
4061 }
4062
4063 /// Create an expression representing a fixed precision decimal number.
4064 /// This function does not perform error checking on the source string,
4065 /// it creates an expression that calls the `decimal` constructor.
4066 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4067 let src_expr = ast::Expr::val(src.as_ref());
4068 Self(ast::Expr::call_extension_fn(
4069 decimal_extension_name(),
4070 vec![src_expr],
4071 ))
4072 }
4073
4074 /// Create an expression representing a particular instant of time.
4075 /// This function does not perform error checking on the source string,
4076 /// it creates an expression that calls the `datetime` constructor.
4077 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4078 let src_expr = ast::Expr::val(src.as_ref());
4079 Self(ast::Expr::call_extension_fn(
4080 datetime_extension_name(),
4081 vec![src_expr],
4082 ))
4083 }
4084
4085 /// Create an expression representing a duration of time.
4086 /// This function does not perform error checking on the source string,
4087 /// it creates an expression that calls the `datetime` constructor.
4088 pub fn new_duration(src: impl AsRef<str>) -> Self {
4089 let src_expr = ast::Expr::val(src.as_ref());
4090 Self(ast::Expr::call_extension_fn(
4091 duration_extension_name(),
4092 vec![src_expr],
4093 ))
4094 }
4095}
4096
4097#[cfg(test)]
4098impl Expression {
4099 /// Deconstruct an [`Expression`] to get the internal type.
4100 /// This function is only intended to be used internally.
4101 pub(crate) fn into_inner(self) -> ast::Expr {
4102 self.0
4103 }
4104}
4105
4106impl FromStr for Expression {
4107 type Err = ParseErrors;
4108
4109 /// create an Expression using Cedar syntax
4110 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4111 ast::Expr::from_str(expression)
4112 .map(Expression)
4113 .map_err(Into::into)
4114 }
4115}
4116
4117impl std::fmt::Display for Expression {
4118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4119 write!(f, "{}", &self.0)
4120 }
4121}
4122
4123/// "Restricted" expressions are used for attribute values and `context`.
4124///
4125/// Restricted expressions can contain only the following:
4126/// - bool, int, and string literals
4127/// - literal `EntityUid`s such as `User::"alice"`
4128/// - extension function calls, where the arguments must be other things
4129/// on this list
4130/// - set and record literals, where the values must be other things on
4131/// this list
4132///
4133/// That means the following are not allowed in restricted expressions:
4134/// - `principal`, `action`, `resource`, `context`
4135/// - builtin operators and functions, including `.`, `in`, `has`, `like`,
4136/// `.contains()`
4137/// - if-then-else expressions
4138#[repr(transparent)]
4139#[derive(Debug, Clone, RefCast, PartialEq, Eq)]
4140pub struct RestrictedExpression(pub(crate) ast::RestrictedExpr);
4141
4142#[doc(hidden)] // because this converts to a private/internal type
4143impl AsRef<ast::RestrictedExpr> for RestrictedExpression {
4144 fn as_ref(&self) -> &ast::RestrictedExpr {
4145 &self.0
4146 }
4147}
4148
4149impl RestrictedExpression {
4150 /// Create an expression representing a literal string.
4151 pub fn new_string(value: String) -> Self {
4152 Self(ast::RestrictedExpr::val(value))
4153 }
4154
4155 /// Create an expression representing a literal bool.
4156 pub fn new_bool(value: bool) -> Self {
4157 Self(ast::RestrictedExpr::val(value))
4158 }
4159
4160 /// Create an expression representing a literal long.
4161 pub fn new_long(value: ast::Integer) -> Self {
4162 Self(ast::RestrictedExpr::val(value))
4163 }
4164
4165 /// Create an expression representing a literal `EntityUid`.
4166 pub fn new_entity_uid(value: EntityUid) -> Self {
4167 Self(ast::RestrictedExpr::val(ast::EntityUID::from(value)))
4168 }
4169
4170 /// Create an expression representing a record.
4171 ///
4172 /// Error if any key appears two or more times in `fields`.
4173 pub fn new_record(
4174 fields: impl IntoIterator<Item = (String, Self)>,
4175 ) -> Result<Self, ExpressionConstructionError> {
4176 Ok(Self(ast::RestrictedExpr::record(
4177 fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4178 )?))
4179 }
4180
4181 /// Create an expression representing a Set.
4182 pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
4183 Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
4184 }
4185
4186 /// Create an expression representing an ip address.
4187 /// This function does not perform error checking on the source string,
4188 /// it creates an expression that calls the `ip` constructor.
4189 pub fn new_ip(src: impl AsRef<str>) -> Self {
4190 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4191 Self(ast::RestrictedExpr::call_extension_fn(
4192 ip_extension_name(),
4193 [src_expr],
4194 ))
4195 }
4196
4197 /// Create an expression representing a fixed precision decimal number.
4198 /// This function does not perform error checking on the source string,
4199 /// it creates an expression that calls the `decimal` constructor.
4200 pub fn new_decimal(src: impl AsRef<str>) -> Self {
4201 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4202 Self(ast::RestrictedExpr::call_extension_fn(
4203 decimal_extension_name(),
4204 [src_expr],
4205 ))
4206 }
4207
4208 /// Create an expression representing a particular instant of time.
4209 /// This function does not perform error checking on the source string,
4210 /// it creates an expression that calls the `datetime` constructor.
4211 pub fn new_datetime(src: impl AsRef<str>) -> Self {
4212 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4213 Self(ast::RestrictedExpr::call_extension_fn(
4214 datetime_extension_name(),
4215 [src_expr],
4216 ))
4217 }
4218
4219 /// Create an expression representing a duration of time.
4220 /// This function does not perform error checking on the source string,
4221 /// it creates an expression that calls the `datetime` constructor.
4222 pub fn new_duration(src: impl AsRef<str>) -> Self {
4223 let src_expr = ast::RestrictedExpr::val(src.as_ref());
4224 Self(ast::RestrictedExpr::call_extension_fn(
4225 duration_extension_name(),
4226 [src_expr],
4227 ))
4228 }
4229
4230 /// Create an unknown expression
4231 #[cfg(feature = "partial-eval")]
4232 pub fn new_unknown(name: impl AsRef<str>) -> Self {
4233 Self(ast::RestrictedExpr::unknown(ast::Unknown::new_untyped(
4234 name.as_ref(),
4235 )))
4236 }
4237}
4238
4239#[cfg(test)]
4240impl RestrictedExpression {
4241 /// Deconstruct an [`RestrictedExpression`] to get the internal type.
4242 /// This function is only intended to be used internally.
4243 pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
4244 self.0
4245 }
4246}
4247
4248fn decimal_extension_name() -> ast::Name {
4249 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4250 #[allow(clippy::unwrap_used)]
4251 ast::Name::unqualified_name("decimal".parse().unwrap())
4252}
4253
4254fn ip_extension_name() -> ast::Name {
4255 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4256 #[allow(clippy::unwrap_used)]
4257 ast::Name::unqualified_name("ip".parse().unwrap())
4258}
4259
4260fn datetime_extension_name() -> ast::Name {
4261 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4262 #[allow(clippy::unwrap_used)]
4263 ast::Name::unqualified_name("datetime".parse().unwrap())
4264}
4265
4266fn duration_extension_name() -> ast::Name {
4267 // PANIC SAFETY: This is a constant and is known to be safe, verified by a test
4268 #[allow(clippy::unwrap_used)]
4269 ast::Name::unqualified_name("duration".parse().unwrap())
4270}
4271
4272impl FromStr for RestrictedExpression {
4273 type Err = RestrictedExpressionParseError;
4274
4275 /// create a `RestrictedExpression` using Cedar syntax
4276 fn from_str(expression: &str) -> Result<Self, Self::Err> {
4277 ast::RestrictedExpr::from_str(expression)
4278 .map(RestrictedExpression)
4279 .map_err(Into::into)
4280 }
4281}
4282
4283/// Builder for a [`Request`]
4284///
4285/// The default for principal, action, resource, and context fields is Unknown
4286/// for partial evaluation.
4287#[doc = include_str!("../experimental_warning.md")]
4288#[cfg(feature = "partial-eval")]
4289#[derive(Debug, Clone)]
4290pub struct RequestBuilder<S> {
4291 principal: ast::EntityUIDEntry,
4292 action: ast::EntityUIDEntry,
4293 resource: ast::EntityUIDEntry,
4294 /// Here, `None` means unknown
4295 context: Option<ast::Context>,
4296 schema: S,
4297}
4298
4299/// A marker type that indicates [`Schema`] is not set for a request
4300#[doc = include_str!("../experimental_warning.md")]
4301#[cfg(feature = "partial-eval")]
4302#[derive(Debug, Clone, Copy)]
4303pub struct UnsetSchema;
4304
4305#[cfg(feature = "partial-eval")]
4306impl Default for RequestBuilder<UnsetSchema> {
4307 fn default() -> Self {
4308 Self {
4309 principal: ast::EntityUIDEntry::unknown(),
4310 action: ast::EntityUIDEntry::unknown(),
4311 resource: ast::EntityUIDEntry::unknown(),
4312 context: None,
4313 schema: UnsetSchema,
4314 }
4315 }
4316}
4317
4318#[cfg(feature = "partial-eval")]
4319impl<S> RequestBuilder<S> {
4320 /// Set the principal.
4321 ///
4322 /// Note that you can create the `EntityUid` using `.parse()` on any
4323 /// string (via the `FromStr` implementation for `EntityUid`).
4324 #[must_use]
4325 pub fn principal(self, principal: EntityUid) -> Self {
4326 Self {
4327 principal: ast::EntityUIDEntry::known(principal.into(), None),
4328 ..self
4329 }
4330 }
4331
4332 /// Set the principal to be unknown, but known to belong to a certain entity type.
4333 ///
4334 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4335 #[must_use]
4336 pub fn unknown_principal_with_type(self, principal_type: EntityTypeName) -> Self {
4337 Self {
4338 principal: ast::EntityUIDEntry::unknown_with_type(principal_type.0, None),
4339 ..self
4340 }
4341 }
4342
4343 /// Set the action.
4344 ///
4345 /// Note that you can create the `EntityUid` using `.parse()` on any
4346 /// string (via the `FromStr` implementation for `EntityUid`).
4347 #[must_use]
4348 pub fn action(self, action: EntityUid) -> Self {
4349 Self {
4350 action: ast::EntityUIDEntry::known(action.into(), None),
4351 ..self
4352 }
4353 }
4354
4355 /// Set the resource.
4356 ///
4357 /// Note that you can create the `EntityUid` using `.parse()` on any
4358 /// string (via the `FromStr` implementation for `EntityUid`).
4359 #[must_use]
4360 pub fn resource(self, resource: EntityUid) -> Self {
4361 Self {
4362 resource: ast::EntityUIDEntry::known(resource.into(), None),
4363 ..self
4364 }
4365 }
4366
4367 /// Set the resource to be unknown, but known to belong to a certain entity type.
4368 ///
4369 /// This information is taken into account when evaluating 'is', '==' and '!=' expressions.
4370 #[must_use]
4371 pub fn unknown_resource_with_type(self, resource_type: EntityTypeName) -> Self {
4372 Self {
4373 resource: ast::EntityUIDEntry::unknown_with_type(resource_type.0, None),
4374 ..self
4375 }
4376 }
4377
4378 /// Set the context.
4379 #[must_use]
4380 pub fn context(self, context: Context) -> Self {
4381 Self {
4382 context: Some(context.0),
4383 ..self
4384 }
4385 }
4386}
4387
4388#[cfg(feature = "partial-eval")]
4389impl RequestBuilder<UnsetSchema> {
4390 /// Set the schema. If present, this will be used for request validation.
4391 #[must_use]
4392 pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
4393 RequestBuilder {
4394 principal: self.principal,
4395 action: self.action,
4396 resource: self.resource,
4397 context: self.context,
4398 schema,
4399 }
4400 }
4401
4402 /// Create the [`Request`]
4403 pub fn build(self) -> Request {
4404 Request(ast::Request::new_unchecked(
4405 self.principal,
4406 self.action,
4407 self.resource,
4408 self.context,
4409 ))
4410 }
4411}
4412
4413#[cfg(feature = "partial-eval")]
4414impl RequestBuilder<&Schema> {
4415 /// Create the [`Request`]
4416 pub fn build(self) -> Result<Request, RequestValidationError> {
4417 Ok(Request(ast::Request::new_with_unknowns(
4418 self.principal,
4419 self.action,
4420 self.resource,
4421 self.context,
4422 Some(&self.schema.0),
4423 Extensions::all_available(),
4424 )?))
4425 }
4426}
4427
4428/// An authorization request is a tuple `<P, A, R, C>` where
4429/// * P is the principal [`EntityUid`],
4430/// * A is the action [`EntityUid`],
4431/// * R is the resource [`EntityUid`], and
4432/// * C is the request [`Context`] record.
4433///
4434/// It represents an authorization request asking the question, "Can this
4435/// principal take this action on this resource in this context?"
4436#[repr(transparent)]
4437#[derive(Debug, Clone, RefCast)]
4438pub struct Request(pub(crate) ast::Request);
4439
4440#[doc(hidden)] // because this converts to a private/internal type
4441impl AsRef<ast::Request> for Request {
4442 fn as_ref(&self) -> &ast::Request {
4443 &self.0
4444 }
4445}
4446
4447#[doc(hidden)]
4448impl From<ast::Request> for Request {
4449 fn from(req: ast::Request) -> Self {
4450 Self(req)
4451 }
4452}
4453
4454impl Request {
4455 /// Create a [`RequestBuilder`]
4456 #[doc = include_str!("../experimental_warning.md")]
4457 #[cfg(feature = "partial-eval")]
4458 pub fn builder() -> RequestBuilder<UnsetSchema> {
4459 RequestBuilder::default()
4460 }
4461
4462 /// Create a Request.
4463 ///
4464 /// Note that you can create the `EntityUid`s using `.parse()` on any
4465 /// string (via the `FromStr` implementation for `EntityUid`).
4466 /// The principal, action, and resource fields are optional to support
4467 /// the case where these fields do not contribute to authorization
4468 /// decisions (e.g., because they are not used in your policies).
4469 /// If any of the fields are `None`, we will automatically generate
4470 /// a unique entity UID that is not equal to any UID in the store.
4471 ///
4472 /// If `schema` is present, this constructor will validate that the
4473 /// `Request` complies with the given `schema`.
4474 pub fn new(
4475 principal: EntityUid,
4476 action: EntityUid,
4477 resource: EntityUid,
4478 context: Context,
4479 schema: Option<&Schema>,
4480 ) -> Result<Self, RequestValidationError> {
4481 Ok(Self(ast::Request::new(
4482 (principal.into(), None),
4483 (action.into(), None),
4484 (resource.into(), None),
4485 context.0,
4486 schema.map(|schema| &schema.0),
4487 Extensions::all_available(),
4488 )?))
4489 }
4490
4491 /// Get the context component of the request. Returns `None` if the context is
4492 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4493 pub fn context(&self) -> Option<&Context> {
4494 self.0.context().map(Context::ref_cast)
4495 }
4496
4497 /// Get the principal component of the request. Returns `None` if the principal is
4498 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4499 pub fn principal(&self) -> Option<&EntityUid> {
4500 match self.0.principal() {
4501 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4502 ast::EntityUIDEntry::Unknown { .. } => None,
4503 }
4504 }
4505
4506 /// Get the action component of the request. Returns `None` if the action is
4507 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4508 pub fn action(&self) -> Option<&EntityUid> {
4509 match self.0.action() {
4510 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4511 ast::EntityUIDEntry::Unknown { .. } => None,
4512 }
4513 }
4514
4515 /// Get the resource component of the request. Returns `None` if the resource is
4516 /// "unknown" (i.e., constructed using the partial evaluation APIs).
4517 pub fn resource(&self) -> Option<&EntityUid> {
4518 match self.0.resource() {
4519 ast::EntityUIDEntry::Known { euid, .. } => Some(EntityUid::ref_cast(euid.as_ref())),
4520 ast::EntityUIDEntry::Unknown { .. } => None,
4521 }
4522 }
4523}
4524
4525/// the Context object for an authorization request
4526#[repr(transparent)]
4527#[derive(Debug, Clone, RefCast)]
4528pub struct Context(ast::Context);
4529
4530#[doc(hidden)] // because this converts to a private/internal type
4531impl AsRef<ast::Context> for Context {
4532 fn as_ref(&self) -> &ast::Context {
4533 &self.0
4534 }
4535}
4536
4537impl Context {
4538 /// Create an empty `Context`
4539 /// ```
4540 /// # use cedar_policy::Context;
4541 /// let context = Context::empty();
4542 /// ```
4543 pub fn empty() -> Self {
4544 Self(ast::Context::empty())
4545 }
4546
4547 /// Create a `Context` from a map of key to "restricted expression",
4548 /// or a Vec of `(key, restricted expression)` pairs, or any other iterator
4549 /// of `(key, restricted expression)` pairs.
4550 /// ```
4551 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4552 /// # use std::str::FromStr;
4553 /// let context = Context::from_pairs([
4554 /// ("key".to_string(), RestrictedExpression::from_str(r#""value""#).unwrap()),
4555 /// ("age".to_string(), RestrictedExpression::from_str("18").unwrap()),
4556 /// ]).unwrap();
4557 /// # // create a request
4558 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4559 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4560 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4561 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4562 /// ```
4563 pub fn from_pairs(
4564 pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
4565 ) -> Result<Self, ContextCreationError> {
4566 Ok(Self(ast::Context::from_pairs(
4567 pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
4568 Extensions::all_available(),
4569 )?))
4570 }
4571
4572 /// Retrieves a value from the Context by its key.
4573 ///
4574 /// # Arguments
4575 ///
4576 /// * `key` - The key to look up in the context
4577 ///
4578 /// # Returns
4579 ///
4580 /// * `Some(EvalResult)` - If the key exists in the context, returns its value
4581 /// * `None` - If the key doesn't exist or if the context is not a Value type
4582 ///
4583 /// # Examples
4584 ///
4585 /// ```
4586 /// # use cedar_policy::{Context, Request, EntityUid};
4587 /// # use std::str::FromStr;
4588 /// let context = Context::from_json_str(r#"{"rayId": "abc123"}"#, None).unwrap();
4589 /// if let Some(value) = context.get("rayId") {
4590 /// // value here is an EvalResult, convertible from the internal Value type
4591 /// println!("Found value: {:?}", value);
4592 /// }
4593 /// assert_eq!(context.get("nonexistent"), None);
4594 /// ```
4595 pub fn get(&self, key: &str) -> Option<EvalResult> {
4596 match &self.0 {
4597 ast::Context::Value(map) => map.get(key).map(|v| EvalResult::from(v.clone())),
4598 ast::Context::RestrictedResidual(_) => None,
4599 }
4600 }
4601
4602 /// Create a `Context` from a string containing JSON (which must be a JSON
4603 /// object, not any other JSON type, or you will get an error here).
4604 /// JSON here must use the `__entity` and `__extn` escapes for entity
4605 /// references, extension values, etc.
4606 ///
4607 /// If a `schema` is provided, this will inform the parsing: for instance, it
4608 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4609 /// if attributes have the wrong types (e.g., string instead of integer).
4610 /// Since different Actions have different schemas for `Context`, you also
4611 /// must specify the `Action` for schema-based parsing.
4612 /// ```
4613 /// # use cedar_policy::{Context, EntityUid, RestrictedExpression, Request};
4614 /// # use std::str::FromStr;
4615 /// let json_data = r#"{
4616 /// "sub": "1234",
4617 /// "groups": {
4618 /// "1234": {
4619 /// "group_id": "abcd",
4620 /// "group_name": "test-group"
4621 /// }
4622 /// }
4623 /// }"#;
4624 /// let context = Context::from_json_str(json_data, None).unwrap();
4625 /// # // create a request
4626 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4627 /// # let a = EntityUid::from_str(r#"Action::"view""#).unwrap();
4628 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4629 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4630 /// ```
4631 pub fn from_json_str(
4632 json: &str,
4633 schema: Option<(&Schema, &EntityUid)>,
4634 ) -> Result<Self, ContextJsonError> {
4635 let schema = schema
4636 .map(|(s, uid)| Self::get_context_schema(s, uid))
4637 .transpose()?;
4638 let context = cedar_policy_core::entities::ContextJsonParser::new(
4639 schema.as_ref(),
4640 Extensions::all_available(),
4641 )
4642 .from_json_str(json)?;
4643 Ok(Self(context))
4644 }
4645
4646 /// Create a `Context` from a `serde_json::Value` (which must be a JSON object,
4647 /// not any other JSON type, or you will get an error here).
4648 /// JSON here must use the `__entity` and `__extn` escapes for entity
4649 /// references, extension values, etc.
4650 ///
4651 /// If a `schema` is provided, this will inform the parsing: for instance, it
4652 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4653 /// if attributes have the wrong types (e.g., string instead of integer).
4654 /// Since different Actions have different schemas for `Context`, you also
4655 /// must specify the `Action` for schema-based parsing.
4656 /// ```
4657 /// # use cedar_policy::{Context, EntityUid, EntityId, EntityTypeName, RestrictedExpression, Request, Schema};
4658 /// # use std::str::FromStr;
4659 /// let schema_json = serde_json::json!(
4660 /// {
4661 /// "": {
4662 /// "entityTypes": {
4663 /// "User": {},
4664 /// "Album": {},
4665 /// },
4666 /// "actions": {
4667 /// "view": {
4668 /// "appliesTo": {
4669 /// "principalTypes": ["User"],
4670 /// "resourceTypes": ["Album"],
4671 /// "context": {
4672 /// "type": "Record",
4673 /// "attributes": {
4674 /// "sub": { "type": "Long" }
4675 /// }
4676 /// }
4677 /// }
4678 /// }
4679 /// }
4680 /// }
4681 /// });
4682 /// let schema = Schema::from_json_value(schema_json).unwrap();
4683 ///
4684 /// let a_eid = EntityId::from_str("view").unwrap();
4685 /// let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4686 /// let action = EntityUid::from_type_name_and_id(a_name, a_eid);
4687 /// let data = serde_json::json!({
4688 /// "sub": 1234
4689 /// });
4690 /// let context = Context::from_json_value(data, Some((&schema, &action))).unwrap();
4691 /// # let p = EntityUid::from_str(r#"User::"alice""#).unwrap();
4692 /// # let r = EntityUid::from_str(r#"Album::"trip""#).unwrap();
4693 /// # let request: Request = Request::new(p, action, r, context, Some(&schema)).unwrap();
4694 /// ```
4695 pub fn from_json_value(
4696 json: serde_json::Value,
4697 schema: Option<(&Schema, &EntityUid)>,
4698 ) -> Result<Self, ContextJsonError> {
4699 let schema = schema
4700 .map(|(s, uid)| Self::get_context_schema(s, uid))
4701 .transpose()?;
4702 let context = cedar_policy_core::entities::ContextJsonParser::new(
4703 schema.as_ref(),
4704 Extensions::all_available(),
4705 )
4706 .from_json_value(json)?;
4707 Ok(Self(context))
4708 }
4709
4710 /// Create a `Context` from a JSON file. The JSON file must contain a JSON
4711 /// object, not any other JSON type, or you will get an error here.
4712 /// JSON here must use the `__entity` and `__extn` escapes for entity
4713 /// references, extension values, etc.
4714 ///
4715 /// If a `schema` is provided, this will inform the parsing: for instance, it
4716 /// will allow `__entity` and `__extn` escapes to be implicit, and it will error
4717 /// if attributes have the wrong types (e.g., string instead of integer).
4718 /// Since different Actions have different schemas for `Context`, you also
4719 /// must specify the `Action` for schema-based parsing.
4720 /// ```no_run
4721 /// # use cedar_policy::{Context, RestrictedExpression};
4722 /// # use cedar_policy::{Entities, EntityId, EntityTypeName, EntityUid, Request,PolicySet};
4723 /// # use std::collections::HashMap;
4724 /// # use std::str::FromStr;
4725 /// # use std::fs::File;
4726 /// let mut json = File::open("json_file.json").unwrap();
4727 /// let context = Context::from_json_file(&json, None).unwrap();
4728 /// # // create a request
4729 /// # let p_eid = EntityId::from_str("alice").unwrap();
4730 /// # let p_name: EntityTypeName = EntityTypeName::from_str("User").unwrap();
4731 /// # let p = EntityUid::from_type_name_and_id(p_name, p_eid);
4732 /// #
4733 /// # let a_eid = EntityId::from_str("view").unwrap();
4734 /// # let a_name: EntityTypeName = EntityTypeName::from_str("Action").unwrap();
4735 /// # let a = EntityUid::from_type_name_and_id(a_name, a_eid);
4736 /// # let r_eid = EntityId::from_str("trip").unwrap();
4737 /// # let r_name: EntityTypeName = EntityTypeName::from_str("Album").unwrap();
4738 /// # let r = EntityUid::from_type_name_and_id(r_name, r_eid);
4739 /// # let request: Request = Request::new(p, a, r, context, None).unwrap();
4740 /// ```
4741 pub fn from_json_file(
4742 json: impl std::io::Read,
4743 schema: Option<(&Schema, &EntityUid)>,
4744 ) -> Result<Self, ContextJsonError> {
4745 let schema = schema
4746 .map(|(s, uid)| Self::get_context_schema(s, uid))
4747 .transpose()?;
4748 let context = cedar_policy_core::entities::ContextJsonParser::new(
4749 schema.as_ref(),
4750 Extensions::all_available(),
4751 )
4752 .from_json_file(json)?;
4753 Ok(Self(context))
4754 }
4755
4756 /// Internal helper function to convert `(&Schema, &EntityUid)` to `impl ContextSchema`
4757 fn get_context_schema(
4758 schema: &Schema,
4759 action: &EntityUid,
4760 ) -> Result<impl ContextSchema, ContextJsonError> {
4761 cedar_policy_core::validator::context_schema_for_action(&schema.0, action.as_ref())
4762 .ok_or_else(|| ContextJsonError::missing_action(action.clone()))
4763 }
4764
4765 /// Merge this [`Context`] with another context (or iterator over
4766 /// `(String, RestrictedExpression)` pairs), returning an error if the two
4767 /// contain overlapping keys
4768 pub fn merge(
4769 self,
4770 other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
4771 ) -> Result<Self, ContextCreationError> {
4772 Self::from_pairs(self.into_iter().chain(other_context))
4773 }
4774
4775 /// Validates this context against the provided schema
4776 ///
4777 /// Returns Ok(()) if the context is valid according to the schema, or an error otherwise
4778 ///
4779 /// This validation is already handled by `Request::new`, so there is no need to separately call
4780 /// if you are validating the whole request
4781 pub fn validate(
4782 &self,
4783 schema: &crate::Schema,
4784 action: &EntityUid,
4785 ) -> std::result::Result<(), RequestValidationError> {
4786 // Call the validate_context function from coreschema.rs
4787 Ok(RequestSchema::validate_context(
4788 &schema.0,
4789 &self.0,
4790 action.as_ref(),
4791 Extensions::all_available(),
4792 )?)
4793 }
4794}
4795
4796/// Utilities for implementing `IntoIterator` for `Context`
4797mod context {
4798 use super::{ast, RestrictedExpression};
4799
4800 /// `IntoIter` iterator for `Context`
4801 #[derive(Debug)]
4802 pub struct IntoIter {
4803 pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
4804 }
4805
4806 impl Iterator for IntoIter {
4807 type Item = (String, RestrictedExpression);
4808
4809 fn next(&mut self) -> Option<Self::Item> {
4810 self.inner
4811 .next()
4812 .map(|(k, v)| (k.to_string(), RestrictedExpression(v)))
4813 }
4814 }
4815}
4816
4817impl IntoIterator for Context {
4818 type Item = (String, RestrictedExpression);
4819
4820 type IntoIter = context::IntoIter;
4821
4822 fn into_iter(self) -> Self::IntoIter {
4823 Self::IntoIter {
4824 inner: self.0.into_iter(),
4825 }
4826 }
4827}
4828
4829#[doc(hidden)]
4830impl From<ast::Context> for Context {
4831 fn from(c: ast::Context) -> Self {
4832 Self(c)
4833 }
4834}
4835
4836impl std::fmt::Display for Request {
4837 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4838 write!(f, "{}", self.0)
4839 }
4840}
4841
4842impl std::fmt::Display for Context {
4843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4844 write!(f, "{}", self.0)
4845 }
4846}
4847
4848/// Result of Evaluation
4849#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4850pub enum EvalResult {
4851 /// Boolean value
4852 Bool(bool),
4853 /// Signed integer value
4854 Long(ast::Integer),
4855 /// String value
4856 String(String),
4857 /// Entity Uid
4858 EntityUid(EntityUid),
4859 /// A first-class set
4860 Set(Set),
4861 /// A first-class anonymous record
4862 Record(Record),
4863 /// An extension value, currently limited to String results
4864 ExtensionValue(String),
4865 // ExtensionValue(std::sync::Arc<dyn InternalExtensionValue>),
4866}
4867
4868/// Sets of Cedar values
4869#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4870pub struct Set(BTreeSet<EvalResult>);
4871
4872impl Set {
4873 /// Iterate over the members of the set
4874 pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
4875 self.0.iter()
4876 }
4877
4878 /// Is a given element in the set
4879 pub fn contains(&self, elem: &EvalResult) -> bool {
4880 self.0.contains(elem)
4881 }
4882
4883 /// Get the number of members of the set
4884 pub fn len(&self) -> usize {
4885 self.0.len()
4886 }
4887
4888 /// Test if the set is empty
4889 pub fn is_empty(&self) -> bool {
4890 self.0.is_empty()
4891 }
4892}
4893
4894/// A record of Cedar values
4895#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)]
4896pub struct Record(BTreeMap<String, EvalResult>);
4897
4898impl Record {
4899 /// Iterate over the attribute/value pairs in the record
4900 pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
4901 self.0.iter()
4902 }
4903
4904 /// Check if a given attribute is in the record
4905 pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
4906 self.0.contains_key(key.as_ref())
4907 }
4908
4909 /// Get a given attribute from the record
4910 pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
4911 self.0.get(key.as_ref())
4912 }
4913
4914 /// Get the number of attributes in the record
4915 pub fn len(&self) -> usize {
4916 self.0.len()
4917 }
4918
4919 /// Test if the record is empty
4920 pub fn is_empty(&self) -> bool {
4921 self.0.is_empty()
4922 }
4923}
4924
4925#[doc(hidden)]
4926impl From<ast::Value> for EvalResult {
4927 fn from(v: ast::Value) -> Self {
4928 match v.value {
4929 ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
4930 ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
4931 ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
4932 ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
4933 Self::EntityUid(ast::EntityUID::clone(&e).into())
4934 }
4935 ast::ValueKind::Set(set) => Self::Set(Set(set
4936 .authoritative
4937 .iter()
4938 .map(|v| v.clone().into())
4939 .collect())),
4940 ast::ValueKind::Record(record) => Self::Record(Record(
4941 record
4942 .iter()
4943 .map(|(k, v)| (k.to_string(), v.clone().into()))
4944 .collect(),
4945 )),
4946 ast::ValueKind::ExtensionValue(ev) => {
4947 Self::ExtensionValue(RestrictedExpr::from(ev.as_ref().clone()).to_string())
4948 }
4949 }
4950 }
4951}
4952
4953#[doc(hidden)]
4954// PANIC SAFETY: see the panic safety comments below
4955#[allow(clippy::fallible_impl_from)]
4956impl From<EvalResult> for Expression {
4957 fn from(res: EvalResult) -> Self {
4958 match res {
4959 EvalResult::Bool(b) => Self::new_bool(b),
4960 EvalResult::Long(l) => Self::new_long(l),
4961 EvalResult::String(s) => Self::new_string(s),
4962 EvalResult::EntityUid(eid) => {
4963 Self::from(ast::Expr::from(ast::Value::from(ast::EntityUID::from(eid))))
4964 }
4965 EvalResult::Set(set) => Self::new_set(set.iter().cloned().map(Self::from)),
4966 EvalResult::Record(r) => {
4967 // PANIC SAFETY: record originates from EvalResult so should not panic when reconstructing as an Expression
4968 #[allow(clippy::unwrap_used)]
4969 Self::new_record(r.iter().map(|(k, v)| (k.clone(), Self::from(v.clone())))).unwrap()
4970 }
4971 EvalResult::ExtensionValue(s) => {
4972 // PANIC SAFETY: the string s is constructed using RestrictedExpr::to_string() so should not panic when being parsed back into a RestrictedExpr
4973 #[allow(clippy::unwrap_used)]
4974 let expr: ast::Expr = ast::RestrictedExpr::from_str(&s).unwrap().into();
4975 Self::from(expr)
4976 }
4977 }
4978 }
4979}
4980
4981impl std::fmt::Display for EvalResult {
4982 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4983 match self {
4984 Self::Bool(b) => write!(f, "{b}"),
4985 Self::Long(l) => write!(f, "{l}"),
4986 Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
4987 Self::EntityUid(uid) => write!(f, "{uid}"),
4988 Self::Set(s) => {
4989 write!(f, "[")?;
4990 for (i, ev) in s.iter().enumerate() {
4991 write!(f, "{ev}")?;
4992 if (i + 1) < s.len() {
4993 write!(f, ", ")?;
4994 }
4995 }
4996 write!(f, "]")?;
4997 Ok(())
4998 }
4999 Self::Record(r) => {
5000 write!(f, "{{")?;
5001 for (i, (k, v)) in r.iter().enumerate() {
5002 write!(f, "\"{}\": {v}", k.escape_debug())?;
5003 if (i + 1) < r.len() {
5004 write!(f, ", ")?;
5005 }
5006 }
5007 write!(f, "}}")?;
5008 Ok(())
5009 }
5010 Self::ExtensionValue(s) => write!(f, "{s}"),
5011 }
5012 }
5013}
5014
5015/// Evaluates an expression.
5016///
5017/// If evaluation results in an error (e.g., attempting to access a non-existent Entity or Record,
5018/// passing the wrong number of arguments to a function etc.), that error is returned as a String
5019pub fn eval_expression(
5020 request: &Request,
5021 entities: &Entities,
5022 expr: &Expression,
5023) -> Result<EvalResult, EvaluationError> {
5024 let all_ext = Extensions::all_available();
5025 let eval = Evaluator::new(request.0.clone(), &entities.0, all_ext);
5026 Ok(EvalResult::from(
5027 // Evaluate under the empty slot map, as an expression should not have slots
5028 eval.interpret(&expr.0, &ast::SlotEnv::new())?,
5029 ))
5030}
5031
5032#[cfg(feature = "tpe")]
5033pub use tpe::*;
5034
5035#[cfg(feature = "tpe")]
5036mod tpe {
5037 use std::collections::{HashMap, HashSet};
5038
5039 use cedar_policy_core::ast::{self, PartialValueToValueError};
5040 use cedar_policy_core::authorizer::Decision;
5041 use cedar_policy_core::batched_evaluator::is_authorized_batched;
5042 use cedar_policy_core::batched_evaluator::{
5043 err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
5044 };
5045 use cedar_policy_core::tpe;
5046 use cedar_policy_core::{
5047 entities::conformance::EntitySchemaConformanceChecker, extensions::Extensions,
5048 validator::CoreSchema,
5049 };
5050 use itertools::Itertools;
5051 use ref_cast::RefCast;
5052
5053 use crate::Entity;
5054 #[cfg(feature = "partial-eval")]
5055 use crate::{
5056 api, tpe_err, Authorizer, Context, Entities, EntityId, EntityTypeName, EntityUid,
5057 PartialRequestCreationError, PermissionQueryError, Policy, PolicySet, Request,
5058 RequestValidationError, RestrictedExpression, Schema, TPEReauthorizationError,
5059 };
5060
5061 /// A partial [`EntityUid`].
5062 /// That is, its [`EntityId`] could be unknown
5063 #[repr(transparent)]
5064 #[derive(Debug, Clone, RefCast)]
5065 pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
5066
5067 impl PartialEntityUid {
5068 /// Construct a [`PartialEntityUid`]
5069 pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
5070 Self(tpe::request::PartialEntityUID {
5071 ty: ty.0,
5072 eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
5073 })
5074 }
5075
5076 /// Construct a [`PartialEntityUid`] from a concrete [`EntityUid`].
5077 pub fn from_concrete(euid: EntityUid) -> Self {
5078 let (ty, eid) = euid.0.components();
5079 Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
5080 }
5081 }
5082
5083 /// A partial [`Request`]
5084 /// Its principal/resource types and action must be known and its context
5085 /// must either be fully known or unknown
5086 #[repr(transparent)]
5087 #[derive(Debug, Clone, RefCast)]
5088 pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
5089
5090 impl PartialRequest {
5091 /// Construct a valid [`PartialRequest`] according to a [`Schema`]
5092 pub fn new(
5093 principal: PartialEntityUid,
5094 action: EntityUid,
5095 resource: PartialEntityUid,
5096 context: Option<Context>,
5097 schema: &Schema,
5098 ) -> Result<Self, PartialRequestCreationError> {
5099 let context = context
5100 .map(|c| match c.0 {
5101 ast::Context::RestrictedResidual(_) => {
5102 Err(PartialRequestCreationError::ContextContainsUnknowns)
5103 }
5104 ast::Context::Value(m) => Ok(m),
5105 })
5106 .transpose()?;
5107 tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
5108 .map(Self)
5109 .map_err(|e| PartialRequestCreationError::Validation(e.into()))
5110 }
5111 }
5112
5113 /// Like [`PartialRequest`] but only `resource` can be unknown
5114 #[repr(transparent)]
5115 #[derive(Debug, Clone, RefCast)]
5116 pub struct ResourceQueryRequest(pub(crate) PartialRequest);
5117
5118 impl ResourceQueryRequest {
5119 /// Construct a valid [`ResourceQueryRequest`] according to a [`Schema`]
5120 pub fn new(
5121 principal: EntityUid,
5122 action: EntityUid,
5123 resource: EntityTypeName,
5124 context: Context,
5125 schema: &Schema,
5126 ) -> Result<Self, PartialRequestCreationError> {
5127 PartialRequest::new(
5128 PartialEntityUid(principal.0.into()),
5129 action,
5130 PartialEntityUid::new(resource, None),
5131 Some(context),
5132 schema,
5133 )
5134 .map(Self)
5135 }
5136
5137 /// Convert [`ResourceQueryRequest`] to a [`Request`] by providing the resource [`EntityId`]
5138 pub fn to_request(
5139 &self,
5140 resource_id: EntityId,
5141 schema: Option<&Schema>,
5142 ) -> Result<Request, RequestValidationError> {
5143 // PANIC SAFETY: various fields are validated through the constructor
5144 #[allow(clippy::unwrap_used)]
5145 Request::new(
5146 EntityUid(self.0 .0.get_principal().try_into().unwrap()),
5147 EntityUid(self.0 .0.get_action()),
5148 EntityUid::from_type_name_and_id(
5149 EntityTypeName(self.0 .0.get_resource_type()),
5150 resource_id,
5151 ),
5152 Context::from_pairs(
5153 self.0
5154 .0
5155 .get_context_attrs()
5156 .unwrap()
5157 .iter()
5158 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5159 )
5160 .unwrap(),
5161 schema,
5162 )
5163 }
5164 }
5165
5166 /// Like [`PartialRequest`] but only `principal` can be unknown
5167 #[repr(transparent)]
5168 #[derive(Debug, Clone, RefCast)]
5169 pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
5170
5171 impl PrincipalQueryRequest {
5172 /// Construct a valid [`PrincipalQueryRequest`] according to a [`Schema`]
5173 pub fn new(
5174 principal: EntityTypeName,
5175 action: EntityUid,
5176 resource: EntityUid,
5177 context: Context,
5178 schema: &Schema,
5179 ) -> Result<Self, PartialRequestCreationError> {
5180 PartialRequest::new(
5181 PartialEntityUid::new(principal, None),
5182 action,
5183 PartialEntityUid(resource.0.into()),
5184 Some(context),
5185 schema,
5186 )
5187 .map(Self)
5188 }
5189
5190 /// Convert [`PrincipalQueryRequest`] to a [`Request`] by providing the principal [`EntityId`]
5191 pub fn to_request(
5192 &self,
5193 principal_id: EntityId,
5194 schema: Option<&Schema>,
5195 ) -> Result<Request, RequestValidationError> {
5196 // PANIC SAFETY: various fields are validated through the constructor
5197 #[allow(clippy::unwrap_used)]
5198 Request::new(
5199 EntityUid::from_type_name_and_id(
5200 EntityTypeName(self.0 .0.get_principal_type()),
5201 principal_id,
5202 ),
5203 EntityUid(self.0 .0.get_action()),
5204 EntityUid(self.0 .0.get_resource().try_into().unwrap()),
5205 Context::from_pairs(
5206 self.0
5207 .0
5208 .get_context_attrs()
5209 .unwrap()
5210 .iter()
5211 .map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
5212 )
5213 .unwrap(),
5214 schema,
5215 )
5216 }
5217 }
5218
5219 /// Partial [`Entities`]
5220 #[repr(transparent)]
5221 #[derive(Debug, Clone, RefCast)]
5222 pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
5223
5224 impl PartialEntities {
5225 /// Construct [`PartialEntities`] from a JSON value
5226 /// The `parent`, `attrs`, `tags` field must be either fully known or
5227 /// unknown. And parent entities cannot have unknown parents.
5228 pub fn from_json_value(
5229 value: serde_json::Value,
5230 schema: &Schema,
5231 ) -> Result<Self, tpe_err::EntitiesError> {
5232 tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
5233 }
5234
5235 /// Construct `[PartialEntities]` given a fully concrete `[Entities]`
5236 pub fn from_concrete(entities: Entities) -> Result<Self, PartialValueToValueError> {
5237 tpe::entities::PartialEntities::try_from(entities.0).map(Self)
5238 }
5239 }
5240
5241 /// A partial version of [`crate::Response`].
5242 #[repr(transparent)]
5243 #[derive(Debug, Clone, RefCast)]
5244 pub struct TPEResponse<'a>(pub(crate) tpe::response::Response<'a>);
5245
5246 impl TPEResponse<'_> {
5247 /// Attempt to get the authorization decision
5248 pub fn decision(&self) -> Option<Decision> {
5249 self.0.decision()
5250 }
5251
5252 /// Perform reauthorization
5253 pub fn reauthorize(
5254 &self,
5255 request: &Request,
5256 entities: &Entities,
5257 ) -> Result<api::Response, TPEReauthorizationError> {
5258 self.0
5259 .reauthorize(&request.0, &entities.0)
5260 .map(Into::into)
5261 .map_err(Into::into)
5262 }
5263 }
5264
5265 /// Entity loader trait for batched evaluation.
5266 ///
5267 /// Loads entities on demand, returning `None` for missing entities.
5268 /// The `load_entities` function must load all requested entities,
5269 /// and must compute and include all ancestors of the requested entities.
5270 /// Loading more entities than requested is allowed.
5271 pub trait EntityLoader {
5272 /// Load all entities for the given set of entity UIDs.
5273 /// Returns a map from [`EntityUid`] to [`Option<Entity>`], where `None` indicates
5274 /// the entity does not exist.
5275 fn load_entities(
5276 &mut self,
5277 uids: &HashSet<EntityUid>,
5278 ) -> HashMap<EntityUid, Option<Entity>>;
5279 }
5280
5281 /// Wrapper struct used to convert an [`EntityLoader`] to an `EntityLoaderInternal`
5282 struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
5283
5284 impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
5285 fn load_entities(
5286 &mut self,
5287 uids: &HashSet<ast::EntityUID>,
5288 ) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
5289 let ids = uids
5290 .iter()
5291 .map(|id| EntityUid::ref_cast(id).clone())
5292 .collect();
5293 self.0
5294 .load_entities(&ids)
5295 .into_iter()
5296 .map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
5297 .collect()
5298 }
5299 }
5300
5301 /// Simple entity loader implementation that loads from a pre-existing Entities store
5302 #[derive(Debug)]
5303
5304 pub struct TestEntityLoader<'a> {
5305 entities: &'a Entities,
5306 }
5307
5308 impl<'a> TestEntityLoader<'a> {
5309 /// Create a new [`TestEntityLoader`] from an existing Entities store
5310 pub fn new(entities: &'a Entities) -> Self {
5311 Self { entities }
5312 }
5313 }
5314
5315 impl EntityLoader for TestEntityLoader<'_> {
5316 fn load_entities(
5317 &mut self,
5318 uids: &HashSet<EntityUid>,
5319 ) -> HashMap<EntityUid, Option<Entity>> {
5320 uids.iter()
5321 .map(|uid| {
5322 let entity = self.entities.get(uid).cloned();
5323 (uid.clone(), entity)
5324 })
5325 .collect()
5326 }
5327 }
5328
5329 impl PolicySet {
5330 /// Perform type-aware partial evaluation on this [`PolicySet`]
5331 /// If successful, the result is a [`PolicySet`] containing residual
5332 /// policies ready for re-authorization
5333 pub fn tpe<'a>(
5334 &self,
5335 request: &'a PartialRequest,
5336 entities: &'a PartialEntities,
5337 schema: &'a Schema,
5338 ) -> Result<TPEResponse<'a>, tpe_err::TPEError> {
5339 use cedar_policy_core::tpe::is_authorized;
5340 let ps = &self.ast;
5341 let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
5342 Ok(TPEResponse(res))
5343 }
5344
5345 /// Like [`Authorizer::is_authorized`] but uses an [`EntityLoader`] to load
5346 /// entities on demand.
5347 ///
5348 /// Calls `loader` at most `max_iters` times, returning
5349 /// early if an authorization result is reached.
5350 /// Otherwise, it iterates `max_iters` times and returns
5351 /// a partial result.
5352 ///
5353 pub fn is_authorized_batched(
5354 &self,
5355 query: &Request,
5356 schema: &Schema,
5357 loader: &mut dyn EntityLoader,
5358 max_iters: u32,
5359 ) -> Result<Decision, BatchedEvalError> {
5360 is_authorized_batched(
5361 &query.0,
5362 &self.ast,
5363 &schema.0,
5364 &mut EntityLoaderWrapper(loader),
5365 max_iters,
5366 )
5367 }
5368
5369 /// Perform a permission query on the resource
5370 pub fn query_resource(
5371 &self,
5372 request: &ResourceQueryRequest,
5373 entities: &Entities,
5374 schema: &Schema,
5375 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5376 let partial_entities = PartialEntities(entities.clone().0.try_into()?);
5377 let core_schema = CoreSchema::new(&schema.0);
5378 let validator =
5379 EntitySchemaConformanceChecker::new(&core_schema, Extensions::all_available());
5380 // We need to type-check the entities
5381 for entity in entities.0.iter() {
5382 validator.validate_entity(entity)?;
5383 }
5384 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5385 // PANIC SAFETY: policy set construction should succeed because there shouldn't be any policy id conflicts
5386 #[allow(clippy::unwrap_used)]
5387 let policies = &Self::from_policies(
5388 residuals
5389 .0
5390 .residual_policies()
5391 .into_iter()
5392 .map(Policy::from_ast),
5393 )
5394 .unwrap();
5395 // PANIC SAFETY: request construction should succeed because each entity passes validation
5396 #[allow(clippy::unwrap_used)]
5397 match residuals.decision() {
5398 Some(Decision::Allow) => Ok(entities
5399 .iter()
5400 .filter(|entity| {
5401 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5402 })
5403 .map(super::Entity::uid)
5404 .collect_vec()
5405 .into_iter()),
5406 Some(Decision::Deny) => Ok(vec![].into_iter()),
5407 None => Ok(entities
5408 .iter()
5409 .filter(|entity| {
5410 entity.0.uid().entity_type() == &request.0 .0.get_resource_type()
5411 })
5412 .filter(|entity| {
5413 let authorizer = Authorizer::new();
5414 authorizer
5415 .is_authorized(
5416 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5417 policies,
5418 entities,
5419 )
5420 .decision
5421 == Decision::Allow
5422 })
5423 .map(super::Entity::uid)
5424 .collect_vec()
5425 .into_iter()),
5426 }
5427 }
5428
5429 /// Perform a permission query on the principal
5430 pub fn query_principal(
5431 &self,
5432 request: &PrincipalQueryRequest,
5433 entities: &Entities,
5434 schema: &Schema,
5435 ) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
5436 let partial_entities = PartialEntities(entities.clone().0.try_into()?);
5437 let core_schema = CoreSchema::new(&schema.0);
5438 let validator =
5439 EntitySchemaConformanceChecker::new(&core_schema, Extensions::all_available());
5440 // We need to type-check the entities
5441 for entity in entities.0.iter() {
5442 validator.validate_entity(entity)?;
5443 }
5444 let residuals = self.tpe(&request.0, &partial_entities, schema)?;
5445 // PANIC SAFETY: policy set construction should succeed because there shouldn't be any policy id conflicts
5446 #[allow(clippy::unwrap_used)]
5447 let policies = &Self::from_policies(
5448 residuals
5449 .0
5450 .residual_policies()
5451 .into_iter()
5452 .map(Policy::from_ast),
5453 )
5454 .unwrap();
5455 // PANIC SAFETY: request construction should succeed because each entity passes validation
5456 #[allow(clippy::unwrap_used)]
5457 match residuals.decision() {
5458 Some(Decision::Allow) => Ok(entities
5459 .iter()
5460 .filter(|entity| {
5461 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5462 })
5463 .map(super::Entity::uid)
5464 .collect_vec()
5465 .into_iter()),
5466 Some(Decision::Deny) => Ok(vec![].into_iter()),
5467 None => Ok(entities
5468 .iter()
5469 .filter(|entity| {
5470 entity.0.uid().entity_type() == &request.0 .0.get_principal_type()
5471 })
5472 .filter(|entity| {
5473 let authorizer = Authorizer::new();
5474 authorizer
5475 .is_authorized(
5476 &request.to_request(entity.uid().id().clone(), None).unwrap(),
5477 policies,
5478 entities,
5479 )
5480 .decision
5481 == Decision::Allow
5482 })
5483 .map(super::Entity::uid)
5484 .collect_vec()
5485 .into_iter()),
5486 }
5487 }
5488 }
5489}
5490
5491// These are the same tests in validator, just ensuring all the plumbing is done correctly
5492#[cfg(test)]
5493mod test_access {
5494 use cedar_policy_core::ast;
5495
5496 use super::*;
5497
5498 fn schema() -> Schema {
5499 let src = r#"
5500 type Task = {
5501 "id": Long,
5502 "name": String,
5503 "state": String,
5504};
5505
5506type T = String;
5507
5508type Tasks = Set<Task>;
5509entity List in [Application] = {
5510 "editors": Team,
5511 "name": String,
5512 "owner": User,
5513 "readers": Team,
5514 "tasks": Tasks,
5515};
5516entity Application;
5517entity User in [Team, Application] = {
5518 "joblevel": Long,
5519 "location": String,
5520};
5521
5522entity CoolList;
5523
5524entity Team in [Team, Application];
5525
5526action Read, Write, Create;
5527
5528action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
5529 principal: [User],
5530 resource : [List]
5531};
5532
5533action GetList in Read appliesTo {
5534 principal : [User],
5535 resource : [List, CoolList]
5536};
5537
5538action GetLists in Read appliesTo {
5539 principal : [User],
5540 resource : [Application]
5541};
5542
5543action CreateList in Create appliesTo {
5544 principal : [User],
5545 resource : [Application]
5546};
5547
5548 "#;
5549
5550 src.parse().unwrap()
5551 }
5552
5553 #[test]
5554 fn principals() {
5555 let schema = schema();
5556 let principals = schema.principals().collect::<HashSet<_>>();
5557 assert_eq!(principals.len(), 1);
5558 let user: EntityTypeName = "User".parse().unwrap();
5559 assert!(principals.contains(&user));
5560 let principals = schema.principals().collect::<Vec<_>>();
5561 assert!(principals.len() > 1);
5562 assert!(principals.iter().all(|ety| **ety == user));
5563 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
5564
5565 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("User").unwrap());
5566 let et = schema.0.get_entity_type(&et).unwrap();
5567 assert!(et.loc.is_some());
5568 }
5569
5570 #[cfg(feature = "extended-schema")]
5571 #[test]
5572 fn common_types_extended() {
5573 use cool_asserts::assert_matches;
5574
5575 use cedar_policy_core::validator::{
5576 types::{EntityRecordKind, Type},
5577 LocatedCommonType,
5578 };
5579
5580 let schema = schema();
5581 assert_eq!(schema.0.common_types().collect::<HashSet<_>>().len(), 3);
5582 let task_type = LocatedCommonType {
5583 name: "Task".into(),
5584 name_loc: None,
5585 type_loc: None,
5586 };
5587 assert!(schema.0.common_types().contains(&task_type));
5588
5589 let tasks_type = LocatedCommonType {
5590 name: "Tasks".into(),
5591 name_loc: None,
5592 type_loc: None,
5593 };
5594 assert!(schema.0.common_types().contains(&tasks_type));
5595 assert!(schema.0.common_types().all(|ct| ct.name_loc.is_some()));
5596 assert!(schema.0.common_types().all(|ct| ct.type_loc.is_some()));
5597
5598 let tasks_type = LocatedCommonType {
5599 name: "T".into(),
5600 name_loc: None,
5601 type_loc: None,
5602 };
5603 assert!(schema.0.common_types().contains(&tasks_type));
5604
5605 let et = ast::EntityType::EntityType(ast::Name::from_normalized_str("List").unwrap());
5606 let et = schema.0.get_entity_type(&et).unwrap();
5607 let attrs = et.attributes();
5608
5609 // Assert that attributes that are resolved from common types still get source locations
5610 let t = attrs.get_attr("tasks").unwrap();
5611 assert!(t.loc.is_some());
5612 assert_matches!(&t.attr_type, cedar_policy_core::validator::types::Type::Set { ref element_type } => {
5613 let el = *element_type.clone().unwrap();
5614 assert_matches!(el, Type::EntityOrRecord(EntityRecordKind::Record { attrs, .. }) => {
5615 assert!(attrs.get_attr("name").unwrap().loc.is_some());
5616 assert!(attrs.get_attr("id").unwrap().loc.is_some());
5617 assert!(attrs.get_attr("state").unwrap().loc.is_some());
5618 });
5619 });
5620 }
5621
5622 #[cfg(feature = "extended-schema")]
5623 #[test]
5624 fn namespace_extended() {
5625 let schema = schema();
5626 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 1);
5627 let default_namespace = schema.0.namespaces().last().unwrap();
5628 assert_eq!(default_namespace.name, SmolStr::from("__cedar"));
5629 assert!(default_namespace.name_loc.is_none());
5630 assert!(default_namespace.def_loc.is_none());
5631 }
5632
5633 #[test]
5634 fn empty_schema_principals_and_resources() {
5635 let empty: Schema = "".parse().unwrap();
5636 assert!(empty.principals().next().is_none());
5637 assert!(empty.resources().next().is_none());
5638 }
5639
5640 #[test]
5641 fn resources() {
5642 let schema = schema();
5643 let resources = schema.resources().cloned().collect::<HashSet<_>>();
5644 let expected: HashSet<EntityTypeName> = HashSet::from([
5645 "List".parse().unwrap(),
5646 "Application".parse().unwrap(),
5647 "CoolList".parse().unwrap(),
5648 ]);
5649 assert_eq!(resources, expected);
5650 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
5651 }
5652
5653 #[test]
5654 fn principals_for_action() {
5655 let schema = schema();
5656 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
5657 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
5658 let got = schema
5659 .principals_for_action(&delete_list)
5660 .unwrap()
5661 .cloned()
5662 .collect::<Vec<_>>();
5663 assert_eq!(got, vec!["User".parse().unwrap()]);
5664 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5665 assert!(schema.principals_for_action(&delete_user).is_none());
5666 }
5667
5668 #[test]
5669 fn resources_for_action() {
5670 let schema = schema();
5671 let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
5672 let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
5673 let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
5674 let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
5675 let got = schema
5676 .resources_for_action(&delete_list)
5677 .unwrap()
5678 .cloned()
5679 .collect::<Vec<_>>();
5680 assert_eq!(got, vec!["List".parse().unwrap()]);
5681 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5682 let got = schema
5683 .resources_for_action(&create_list)
5684 .unwrap()
5685 .cloned()
5686 .collect::<Vec<_>>();
5687 assert_eq!(got, vec!["Application".parse().unwrap()]);
5688 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5689 let got = schema
5690 .resources_for_action(&get_list)
5691 .unwrap()
5692 .cloned()
5693 .collect::<HashSet<_>>();
5694 assert_eq!(
5695 got,
5696 HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
5697 );
5698 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5699 assert!(schema.principals_for_action(&delete_user).is_none());
5700 }
5701
5702 #[test]
5703 fn principal_parents() {
5704 let schema = schema();
5705 let user: EntityTypeName = "User".parse().unwrap();
5706 let parents = schema
5707 .ancestors(&user)
5708 .unwrap()
5709 .cloned()
5710 .collect::<HashSet<_>>();
5711 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5712 let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
5713 assert_eq!(parents, expected);
5714 let parents = schema
5715 .ancestors(&"List".parse().unwrap())
5716 .unwrap()
5717 .cloned()
5718 .collect::<HashSet<_>>();
5719 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5720 let expected = HashSet::from(["Application".parse().unwrap()]);
5721 assert_eq!(parents, expected);
5722 assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
5723 let parents = schema
5724 .ancestors(&"CoolList".parse().unwrap())
5725 .unwrap()
5726 .cloned()
5727 .collect::<HashSet<_>>();
5728 assert!(parents.iter().all(|ety| ety.0.loc().is_some()));
5729 let expected = HashSet::from([]);
5730 assert_eq!(parents, expected);
5731 }
5732
5733 #[test]
5734 fn action_groups() {
5735 let schema = schema();
5736 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
5737 let expected = ["Read", "Write", "Create"]
5738 .into_iter()
5739 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
5740 .collect::<HashSet<EntityUid>>();
5741 #[cfg(feature = "extended-schema")]
5742 assert!(groups.iter().all(|ety| ety.0.loc().is_some()));
5743 assert_eq!(groups, expected);
5744 }
5745
5746 #[test]
5747 fn actions() {
5748 let schema = schema();
5749 let actions = schema.actions().cloned().collect::<HashSet<_>>();
5750 let expected = [
5751 "Read",
5752 "Write",
5753 "Create",
5754 "DeleteList",
5755 "EditShare",
5756 "UpdateList",
5757 "CreateTask",
5758 "UpdateTask",
5759 "DeleteTask",
5760 "GetList",
5761 "GetLists",
5762 "CreateList",
5763 ]
5764 .into_iter()
5765 .map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
5766 .collect::<HashSet<EntityUid>>();
5767 assert_eq!(actions, expected);
5768 #[cfg(feature = "extended-schema")]
5769 assert!(actions.iter().all(|ety| ety.0.loc().is_some()));
5770 }
5771
5772 #[test]
5773 fn entities() {
5774 let schema = schema();
5775 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
5776 let expected = ["List", "Application", "User", "CoolList", "Team"]
5777 .into_iter()
5778 .map(|ty| ty.parse().unwrap())
5779 .collect::<HashSet<EntityTypeName>>();
5780 assert_eq!(entities, expected);
5781 }
5782}
5783
5784#[cfg(test)]
5785mod test_access_namespace {
5786 use super::*;
5787
5788 fn schema() -> Schema {
5789 let src = r#"
5790 namespace Foo {
5791 type Task = {
5792 "id": Long,
5793 "name": String,
5794 "state": String,
5795};
5796
5797type Tasks = Set<Task>;
5798entity List in [Application] = {
5799 "editors": Team,
5800 "name": String,
5801 "owner": User,
5802 "readers": Team,
5803 "tasks": Tasks,
5804};
5805entity Application;
5806entity User in [Team, Application] = {
5807 "joblevel": Long,
5808 "location": String,
5809};
5810
5811entity CoolList;
5812
5813entity Team in [Team, Application];
5814
5815action Read, Write, Create;
5816
5817action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
5818 principal: [User],
5819 resource : [List]
5820};
5821
5822action GetList in Read appliesTo {
5823 principal : [User],
5824 resource : [List, CoolList]
5825};
5826
5827action GetLists in Read appliesTo {
5828 principal : [User],
5829 resource : [Application]
5830};
5831
5832action CreateList in Create appliesTo {
5833 principal : [User],
5834 resource : [Application]
5835};
5836 }
5837
5838 "#;
5839
5840 src.parse().unwrap()
5841 }
5842
5843 #[test]
5844 fn principals() {
5845 let schema = schema();
5846 let principals = schema.principals().collect::<HashSet<_>>();
5847 assert_eq!(principals.len(), 1);
5848 let user: EntityTypeName = "Foo::User".parse().unwrap();
5849 assert!(principals.contains(&user));
5850 let principals = schema.principals().collect::<Vec<_>>();
5851 assert!(principals.len() > 1);
5852 assert!(principals.iter().all(|ety| **ety == user));
5853 assert!(principals.iter().all(|ety| ety.0.loc().is_some()));
5854 }
5855
5856 #[test]
5857 fn empty_schema_principals_and_resources() {
5858 let empty: Schema = "".parse().unwrap();
5859 assert!(empty.principals().next().is_none());
5860 assert!(empty.resources().next().is_none());
5861 }
5862
5863 #[test]
5864 fn resources() {
5865 let schema = schema();
5866 let resources = schema.resources().cloned().collect::<HashSet<_>>();
5867 let expected: HashSet<EntityTypeName> = HashSet::from([
5868 "Foo::List".parse().unwrap(),
5869 "Foo::Application".parse().unwrap(),
5870 "Foo::CoolList".parse().unwrap(),
5871 ]);
5872 assert_eq!(resources, expected);
5873 assert!(resources.iter().all(|ety| ety.0.loc().is_some()));
5874 }
5875
5876 #[test]
5877 fn principals_for_action() {
5878 let schema = schema();
5879 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
5880 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
5881 let got = schema
5882 .principals_for_action(&delete_list)
5883 .unwrap()
5884 .cloned()
5885 .collect::<Vec<_>>();
5886 assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
5887 assert!(schema.principals_for_action(&delete_user).is_none());
5888 }
5889
5890 #[test]
5891 fn resources_for_action() {
5892 let schema = schema();
5893 let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
5894 let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
5895 let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
5896 let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
5897 let got = schema
5898 .resources_for_action(&delete_list)
5899 .unwrap()
5900 .cloned()
5901 .collect::<Vec<_>>();
5902 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5903
5904 assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
5905 let got = schema
5906 .resources_for_action(&create_list)
5907 .unwrap()
5908 .cloned()
5909 .collect::<Vec<_>>();
5910 assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
5911 assert!(got.iter().all(|ety| ety.0.loc().is_some()));
5912
5913 let got = schema
5914 .resources_for_action(&get_list)
5915 .unwrap()
5916 .cloned()
5917 .collect::<HashSet<_>>();
5918 assert_eq!(
5919 got,
5920 HashSet::from([
5921 "Foo::List".parse().unwrap(),
5922 "Foo::CoolList".parse().unwrap()
5923 ])
5924 );
5925 assert!(schema.principals_for_action(&delete_user).is_none());
5926 }
5927
5928 #[test]
5929 fn principal_parents() {
5930 let schema = schema();
5931 let user: EntityTypeName = "Foo::User".parse().unwrap();
5932 let parents = schema
5933 .ancestors(&user)
5934 .unwrap()
5935 .cloned()
5936 .collect::<HashSet<_>>();
5937 let expected = HashSet::from([
5938 "Foo::Team".parse().unwrap(),
5939 "Foo::Application".parse().unwrap(),
5940 ]);
5941 assert_eq!(parents, expected);
5942 let parents = schema
5943 .ancestors(&"Foo::List".parse().unwrap())
5944 .unwrap()
5945 .cloned()
5946 .collect::<HashSet<_>>();
5947 let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
5948 assert_eq!(parents, expected);
5949 assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
5950 let parents = schema
5951 .ancestors(&"Foo::CoolList".parse().unwrap())
5952 .unwrap()
5953 .cloned()
5954 .collect::<HashSet<_>>();
5955 let expected = HashSet::from([]);
5956 assert_eq!(parents, expected);
5957 }
5958
5959 #[test]
5960 fn action_groups() {
5961 let schema = schema();
5962 let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
5963 let expected = ["Read", "Write", "Create"]
5964 .into_iter()
5965 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
5966 .collect::<HashSet<EntityUid>>();
5967 assert_eq!(groups, expected);
5968 }
5969
5970 #[test]
5971 fn actions() {
5972 let schema = schema();
5973 let actions = schema.actions().cloned().collect::<HashSet<_>>();
5974 let expected = [
5975 "Read",
5976 "Write",
5977 "Create",
5978 "DeleteList",
5979 "EditShare",
5980 "UpdateList",
5981 "CreateTask",
5982 "UpdateTask",
5983 "DeleteTask",
5984 "GetList",
5985 "GetLists",
5986 "CreateList",
5987 ]
5988 .into_iter()
5989 .map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
5990 .collect::<HashSet<EntityUid>>();
5991 assert_eq!(actions, expected);
5992 }
5993
5994 #[test]
5995 fn entities() {
5996 let schema = schema();
5997 let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
5998 let expected = [
5999 "Foo::List",
6000 "Foo::Application",
6001 "Foo::User",
6002 "Foo::CoolList",
6003 "Foo::Team",
6004 ]
6005 .into_iter()
6006 .map(|ty| ty.parse().unwrap())
6007 .collect::<HashSet<EntityTypeName>>();
6008 assert_eq!(entities, expected);
6009 }
6010
6011 #[test]
6012 fn test_request_context() {
6013 // Create a context with some test data
6014 let context =
6015 Context::from_json_str(r#"{"testKey": "testValue", "numKey": 42}"#, None).unwrap();
6016
6017 // Create entity UIDs for the request
6018 let principal: EntityUid = "User::\"alice\"".parse().unwrap();
6019 let action: EntityUid = "Action::\"view\"".parse().unwrap();
6020 let resource: EntityUid = "Resource::\"doc123\"".parse().unwrap();
6021
6022 // Create the request
6023 let request = Request::new(
6024 principal, action, resource, context, None, // no schema validation for this test
6025 )
6026 .unwrap();
6027
6028 // Test context() method
6029 let retrieved_context = request.context().expect("Context should be present");
6030
6031 // Test get() method on the retrieved context
6032 assert!(retrieved_context.get("testKey").is_some());
6033 assert!(retrieved_context.get("numKey").is_some());
6034 assert!(retrieved_context.get("nonexistent").is_none());
6035 }
6036
6037 #[cfg(feature = "extended-schema")]
6038 #[test]
6039 fn namespace_extended() {
6040 let schema = schema();
6041 assert_eq!(schema.0.namespaces().collect::<HashSet<_>>().len(), 2);
6042 let default_namespace = schema
6043 .0
6044 .namespaces()
6045 .filter(|n| n.name == *"__cedar")
6046 .last()
6047 .unwrap();
6048 assert!(default_namespace.name_loc.is_none());
6049 assert!(default_namespace.def_loc.is_none());
6050
6051 let default_namespace = schema
6052 .0
6053 .namespaces()
6054 .filter(|n| n.name == *"Foo")
6055 .last()
6056 .unwrap();
6057 assert!(default_namespace.name_loc.is_some());
6058 assert!(default_namespace.def_loc.is_some());
6059 }
6060}
6061
6062#[cfg(test)]
6063mod test_lossless_empty {
6064 use super::{LosslessPolicy, Policy, PolicyId, Template};
6065
6066 #[test]
6067 fn test_lossless_empty_policy() {
6068 const STATIC_POLICY_TEXT: &str = "permit(principal,action,resource);";
6069 let policy0 = Policy::parse(Some(PolicyId::new("policy0")), STATIC_POLICY_TEXT)
6070 .expect("Failed to parse");
6071 let lossy_policy0 = Policy {
6072 ast: policy0.ast.clone(),
6073 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6074 };
6075 // The `to_cedar` representation becomes lossy since we didn't provide text
6076 assert_eq!(
6077 lossy_policy0.to_cedar(),
6078 Some(String::from(
6079 "permit(\n principal,\n action,\n resource\n);"
6080 ))
6081 );
6082 // The EST representation is obtained from the AST
6083 let lossy_policy0_est = lossy_policy0
6084 .lossless
6085 .est(|| policy0.ast.clone().into())
6086 .unwrap();
6087 assert_eq!(lossy_policy0_est, policy0.ast.into());
6088 }
6089
6090 #[test]
6091 fn test_lossless_empty_template() {
6092 const TEMPLATE_TEXT: &str = "permit(principal == ?principal,action,resource);";
6093 let template0 = Template::parse(Some(PolicyId::new("template0")), TEMPLATE_TEXT)
6094 .expect("Failed to parse");
6095 let lossy_template0 = Template {
6096 ast: template0.ast.clone(),
6097 lossless: LosslessPolicy::policy_or_template_text(None::<&str>),
6098 };
6099 // The `to_cedar` representation becomes lossy since we didn't provide text
6100 assert_eq!(
6101 lossy_template0.to_cedar(),
6102 String::from("permit(\n principal == ?principal,\n action,\n resource\n);")
6103 );
6104 // The EST representation is obtained from the AST
6105 let lossy_template0_est = lossy_template0
6106 .lossless
6107 .est(|| template0.ast.clone().into())
6108 .unwrap();
6109 assert_eq!(lossy_template0_est, template0.ast.into());
6110 }
6111}
6112
6113/// Given a schema and policy set, compute an entity manifest.
6114///
6115/// The policies must validate against the schema in strict mode,
6116/// otherwise an error is returned.
6117/// The manifest describes the data required to answer requests
6118/// for each action.
6119#[doc = include_str!("../experimental_warning.md")]
6120#[cfg(feature = "entity-manifest")]
6121pub fn compute_entity_manifest(
6122 validator: &Validator,
6123 pset: &PolicySet,
6124) -> Result<EntityManifest, EntityManifestError> {
6125 entity_manifest::compute_entity_manifest(&validator.0, &pset.ast)
6126 .map_err(std::convert::Into::into)
6127}